using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using JetBrains.Annotations; using RoR2.CameraModes; using RoR2.ConVar; using RoR2.Networking; using RoR2.UI; using UnityEngine; using UnityEngine.Rendering.PostProcessing; using UnityEngine.ResourceManagement.AsyncOperations; namespace RoR2; public class CameraRigController : MonoBehaviour { [Obsolete("CameraMode objects are now used instead of enums.", true)] public enum CameraMode { None, PlayerBasic, Fly, SpectateOrbit, SpectateUser } public struct AimAssistInfo { public HurtBox aimAssistHurtbox; public Vector3 localPositionOnHurtbox; public Vector3 worldPosition; } [Tooltip("The main camera for rendering the scene.")] [Header("Component References")] public Camera sceneCam; [Tooltip("The UI camera.")] public Camera uiCam; [Tooltip("The skybox camera.")] public Camera skyboxCam; [Tooltip("The particle system to play while sprinting.")] public ParticleSystem sprintingParticleSystem; [Tooltip("The particle system to play while skills are disabled.")] public ParticleSystem disabledSkillsParticleSystem; [Header("Camera Parameters")] [Tooltip("The default FOV of this camera.")] public float baseFov = 60f; [Tooltip("The viewport to use.")] public Rect viewport = new Rect(0f, 0f, 1f, 1f); [Tooltip("The maximum distance of the raycast used to determine the aim vector.")] public float maxAimRaycastDistance = 1000f; [Tooltip("If true, treat this camera as though it's in a cutscene")] public bool isCutscene; [Header("Near-Camera Character Dithering")] public bool enableFading = true; public float fadeStartDistance = 1f; public float fadeEndDistance = 4f; [Header("Behavior")] [Tooltip("Whether or not to create a HUD.")] public bool createHud = true; [Tooltip("Whether or not this camera being enabled forces player-owned cameras to be disabled.")] public bool suppressPlayerCameras; [Tooltip("Forces music to be enabled for this camera.")] public bool enableMusic; private ulong musicListenerId; private bool musicEnabled; [Header("Target (for debug only)")] public GameObject nextTarget; public static Action OnChangeHUD; public static Action OnHUDUpdated; private CameraModeBase _cameraMode; private GameObject _target; private CameraState desiredCameraState; public CameraState currentCameraState; public bool doNotOverrideCameraState; private CameraState lerpCameraState; private float lerpCameraTime = 1f; private float lerpCameraTimeScale = 1f; private ICameraStateProvider overrideCam; private CameraModeBase.CameraModeContext cameraModeContext; private NetworkUser _viewer; private LocalUser _localUserViewer; private static GameObject DamageNumberManagerPrefab; private static GameObject HudPrefab; private static string previousHudPath; public CameraTargetParams targetParams; private const string musicSystemDeviceShareSet = "Music_Audio_Device"; public AimAssistInfo lastAimAssist; public AimAssistInfo aimAssist; private static List instancesList = new List(); public static readonly ReadOnlyCollection readOnlyInstancesList = instancesList.AsReadOnly(); public static FloatConVar aimStickExponent = new FloatConVar("aim_stick_exponent", ConVarFlags.None, "1", "The exponent for stick input used for aiming."); public static FloatConVar aimStickDualZoneThreshold = new FloatConVar("aim_stick_dual_zone_threshold", ConVarFlags.None, "0.90", "The threshold for stick dual zone behavior."); public static FloatConVar aimStickDualZoneSlope = new FloatConVar("aim_stick_dual_zone_slope", ConVarFlags.None, "0.40", "The slope value for stick dual zone behavior."); public static FloatConVar aimStickDualZoneSmoothing = new FloatConVar("aim_stick_smoothing", ConVarFlags.None, "0.05", "The smoothing value for stick aiming."); public static FloatConVar aimStickGlobalScale = new FloatConVar("aim_stick_global_scale", ConVarFlags.Archive, "1.00", "The global sensitivity scale for stick aiming."); public static FloatConVar aimStickAssistMinSlowdownScale = new FloatConVar("aim_stick_assist_min_slowdown_scale", ConVarFlags.None, "1", "The MIN amount the sensitivity scales down when passing over an enemy."); public static FloatConVar aimStickAssistMaxSlowdownScale = new FloatConVar("aim_stick_assist_max_slowdown_scale", ConVarFlags.None, "0.4", "The MAX amount the sensitivity scales down when passing over an enemy. UNUSED"); public static FloatConVar aimStickAssistMinDelta = new FloatConVar("aim_stick_assist_min_delta", ConVarFlags.None, "0", "The MIN amount in radians the aim assist will turn towards"); public static FloatConVar aimStickAssistMaxDelta = new FloatConVar("aim_stick_assist_max_delta", ConVarFlags.None, "1.57", "The MAX amount in radians the aim assist will turn towards"); public static FloatConVar aimStickAssistMaxInputHelp = new FloatConVar("aim_stick_assist_max_input_help", ConVarFlags.None, "0.2", "The amount, from 0-1, that the aim assist will actually ADD magnitude towards. Helps you keep target while strafing. CURRENTLY UNUSED."); public static FloatConVar aimStickAssistMaxSize = new FloatConVar("aim_stick_assist_max_size", ConVarFlags.None, "3", "The size, as a coefficient, of the aim assist 'white' zone."); public static FloatConVar aimStickAssistMinSize = new FloatConVar("aim_stick_assist_min_size", ConVarFlags.None, "1", "The minimum size, as a percentage of the GUI, of the aim assist 'red' zone."); public static BoolConVar enableSprintSensitivitySlowdown = new BoolConVar("enable_sprint_sensitivity_slowdown", ConVarFlags.Archive, "1", "Enables sensitivity reduction while sprinting."); private float hitmarkerAlpha; private float hitmarkerTimer; public bool disableSpectating { get; set; } public CameraModeBase cameraMode { get { return _cameraMode; } set { if (_cameraMode != value) { _cameraMode?.OnUninstall(this); _cameraMode = value; _cameraMode?.OnInstall(this); } } } public HUD hud { get; private set; } public GameObject firstPersonTarget { get; private set; } public TeamIndex targetTeamIndex { get; private set; } = TeamIndex.None; public CharacterBody targetBody { get; private set; } public Vector3 rawScreenShakeDisplacement { get; private set; } public Vector3 crosshairWorldPosition { get; private set; } public bool hasOverride => overrideCam != null; public bool isControlAllowed { get { if (hasOverride) { return overrideCam.IsUserControlAllowed(this); } return true; } } public bool isHudAllowed { get { if ((bool)target) { if (hasOverride) { return overrideCam.IsHudAllowed(this); } return true; } return false; } } public GameObject target { get { return _target; } private set { if ((object)_target != value) { GameObject oldTarget = _target; _target = value; bool flag = _target; targetParams = (flag ? target.GetComponent() : null); targetBody = (flag ? target.GetComponent() : null); cameraMode?.OnTargetChanged(this, new CameraModeBase.OnTargetChangedArgs { oldTarget = oldTarget, newTarget = _target }); CameraRigController.onCameraTargetChanged?.Invoke(this, target); } } } public NetworkUser viewer { get { return _viewer; } set { _viewer = value; localUserViewer = (_viewer ? _viewer.localUser : null); } } public LocalUser localUserViewer { get { return _localUserViewer; } private set { if (_localUserViewer != value) { if (_localUserViewer != null) { _localUserViewer.cameraRigController = null; } _localUserViewer = value; if (_localUserViewer != null) { _localUserViewer.cameraRigController = this; } if ((bool)hud) { hud.localUserViewer = _localUserViewer; } } } } public HurtBox lastCrosshairHurtBox { get; private set; } public static event Action onCameraTargetChanged; public static event Action onCameraEnableGlobal; public static event Action onCameraDisableGlobal; private void StartStateLerp(float lerpDuration) { lerpCameraState = currentCameraState; if (lerpDuration > 0f) { lerpCameraTime = 0f; lerpCameraTimeScale = 1f / lerpDuration; } else { lerpCameraTime = 1f; lerpCameraTimeScale = 0f; } } public void SetOverrideCam(ICameraStateProvider newOverrideCam, float lerpDuration = 1f) { if (newOverrideCam != overrideCam) { if (overrideCam != null && newOverrideCam == null) { cameraMode?.MatchState(in cameraModeContext, in currentCameraState); } overrideCam = newOverrideCam; StartStateLerp(lerpDuration); } } public bool IsOverrideCam(ICameraStateProvider testOverrideCam) { return overrideCam == testOverrideCam; } public static IEnumerator InitializeReferences() { AsyncOperationHandle handle2 = LegacyResourcesAPI.LoadAsync("Prefabs/DamageNumberManager"); while (!handle2.IsDone) { yield return null; } DamageNumberManagerPrefab = handle2.Result; previousHudPath = GetHUDPath(); handle2 = LegacyResourcesAPI.LoadAsync(previousHudPath); while (!handle2.IsDone) { yield return null; } HudPrefab = handle2.Result; } private void Start() { RoR2Application.onUpdate += EarlyUpdate; OnChangeHUD = (Action)Delegate.Combine(OnChangeHUD, new Action(UpdateHUD)); UpdateHUD(); if ((bool)uiCam) { uiCam.transform.parent = null; uiCam.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity); } if (!DamageNumberManager.instance) { if (DamageNumberManagerPrefab != null) { UnityEngine.Object.Instantiate(DamageNumberManagerPrefab); } else { AsyncOperationHandle asyncOperationHandle = LegacyResourcesAPI.LoadAsync("Prefabs/DamageNumberManager"); asyncOperationHandle.Completed += delegate(AsyncOperationHandle x) { UnityEngine.Object.Instantiate(x.Result); }; } } currentCameraState = new CameraState { position = base.transform.position, rotation = base.transform.rotation, fov = baseFov }; cameraMode = CameraModePlayerBasic.playerBasic; if (enableMusic && (!RoR2Application.isInLocalMultiPlayer || _viewer.localUser.inputPlayer.name == "PlayerMain")) { EnableMusicForCamera(); } } private void UpdateHUD() { if (createHud) { if ((bool)hud) { UnityEngine.Object.Destroy(hud.gameObject); } string hUDPath = GetHUDPath(); if (HudPrefab == null || hUDPath != previousHudPath) { HudPrefab = LegacyResourcesAPI.Load(hUDPath); previousHudPath = hUDPath; } GameObject gameObject = UnityEngine.Object.Instantiate(HudPrefab); hud = gameObject.GetComponent(); hud.cameraRigController = this; SetPostProcessVolumeExclusive(hud.scoreboardPostProcessVolume); hud.GetComponent().worldCamera = uiCam; hud.GetComponent().cameraRigController = this; hud.localUserViewer = localUserViewer; OnHUDUpdated?.Invoke(); } } [CanBeNull] private static string GetHUDPath() { if (NetworkUser.readOnlyLocalPlayersList == null || NetworkUser.readOnlyLocalPlayersList.Count < 1) { return "Prefabs/HUDSimple"; } UserProfile userProfile = NetworkUser.readOnlyLocalPlayersList[0]?.localUser?.userProfile; if (userProfile != null) { switch ((HudType)userProfile.hudSizeMode) { case HudType.Default: return "Prefabs/HUDSimple"; case HudType.Large: return "Prefabs/HUDSimple_Big"; case HudType.Auto: return "Prefabs/HUDSimple_Big"; default: Debug.LogWarning(string.Format("Undefined {0}. Value : {1}. Resorting to default HUD.", "HudType", userProfile.hudSizeMode)); return "Prefabs/HUDSimple"; } } return "Prefabs/HUDSimple"; } private void OnEnable() { instancesList.Add(this); if ((bool)uiCam) { uiCam.gameObject.SetActive(value: true); } if ((bool)hud) { hud.gameObject.SetActive(value: true); } CameraRigController.onCameraEnableGlobal?.Invoke(this); } private void OnDisable() { CameraRigController.onCameraDisableGlobal?.Invoke(this); if ((bool)uiCam) { uiCam.gameObject.SetActive(value: false); } if ((bool)hud) { hud.gameObject.SetActive(value: false); } instancesList.Remove(this); } private void OnDestroy() { RoR2Application.onUpdate -= EarlyUpdate; OnChangeHUD = (Action)Delegate.Remove(OnChangeHUD, new Action(UpdateHUD)); cameraMode = null; if ((bool)uiCam) { UnityEngine.Object.Destroy(uiCam.gameObject); } if ((bool)hud) { UnityEngine.Object.Destroy(hud.gameObject); } DisableMusicForCamera(); } private void EarlyUpdate() { target = nextTarget; if ((bool)targetBody) { targetTeamIndex = targetBody.teamComponent.teamIndex; } if (Time.deltaTime != 0f && !isCutscene) { lerpCameraTime += Time.deltaTime * lerpCameraTimeScale; firstPersonTarget = null; sceneCam.rect = viewport; GenerateCameraModeContext(out cameraModeContext); if (cameraMode != null) { cameraMode.CollectLookInput(in cameraModeContext, out var result); CameraModeBase cameraModeBase = cameraMode; ref CameraModeBase.CameraModeContext context = ref cameraModeContext; CameraModeBase.ApplyLookInputArgs args = new CameraModeBase.ApplyLookInputArgs { lookInput = result.lookInput }; cameraModeBase.ApplyLookInput(in context, in args); } } } private void LateUpdate() { if (Time.deltaTime == 0f || isCutscene) { return; } CameraState cameraState = currentCameraState; if (cameraMode != null && cameraModeContext.cameraInfo.cameraRigController != null && !doNotOverrideCameraState) { cameraMode.Update(in cameraModeContext, out var result); cameraState = result.cameraState; firstPersonTarget = result.firstPersonTarget; crosshairWorldPosition = result.crosshairWorldPosition; SetParticleSystemActive(result.showSprintParticles, sprintingParticleSystem); SetParticleSystemActive(result.showDisabledSkillsParticles, disabledSkillsParticleSystem); } if ((bool)hud) { CharacterMaster targetMaster = (targetBody ? targetBody.master : null); hud.targetMaster = targetMaster; } CameraState cameraState2 = cameraState; if (overrideCam != null) { if (!(overrideCam is UnityEngine.Object @object) || (bool)@object) { overrideCam.GetCameraState(this, ref cameraState2); } else { overrideCam = null; } } if (lerpCameraTime >= 1f) { currentCameraState = cameraState2; } else { currentCameraState = CameraState.Lerp(ref lerpCameraState, ref cameraState2, RemapLerpTime(lerpCameraTime)); } SetCameraState(currentCameraState); } public void UpdatePreviousCameraState(Vector3 position, Quaternion rotation) { cameraModeContext.cameraInfo.previousCameraState.position = position; cameraModeContext.cameraInfo.previousCameraState.rotation = rotation; } private void GenerateCameraModeContext(out CameraModeBase.CameraModeContext result) { result.cameraInfo = default(CameraModeBase.CameraInfo); result.targetInfo = default(CameraModeBase.TargetInfo); result.viewerInfo = default(CameraModeBase.ViewerInfo); ref CameraModeBase.CameraInfo cameraInfo = ref result.cameraInfo; ref CameraModeBase.TargetInfo targetInfo = ref result.targetInfo; ref CameraModeBase.ViewerInfo viewerInfo = ref result.viewerInfo; cameraInfo.cameraRigController = this; cameraInfo.sceneCam = sceneCam; cameraInfo.overrideCam = overrideCam; cameraInfo.previousCameraState = currentCameraState; cameraInfo.baseFov = baseFov; targetInfo.target = (target ? target : null); targetInfo.body = targetBody; targetInfo.inputBank = targetInfo.target?.GetComponent(); if (targetBody != null && targetBody.currentVehicle != null && targetBody.currentVehicle.targetParams != null) { targetInfo.targetParams = targetBody.currentVehicle.targetParams; } else { targetInfo.targetParams = targetParams; } targetInfo.teamIndex = targetInfo.target?.GetComponent()?.teamIndex ?? TeamIndex.None; targetInfo.isSprinting = (bool)targetInfo.body && targetInfo.body.isSprinting; targetInfo.skillsAreDisabled = (bool)targetInfo.body && targetInfo.body.allSkillsDisabled; targetInfo.master = targetInfo.body?.master; targetInfo.networkUser = targetInfo.master?.playerCharacterMasterController?.networkUser; targetInfo.networkedViewAngles = targetInfo.networkUser?.GetComponent(); targetInfo.isViewerControlled = (bool)targetInfo.networkUser && (object)targetInfo.networkUser == localUserViewer?.currentNetworkUser; viewerInfo.localUser = localUserViewer; viewerInfo.userProfile = localUserViewer?.userProfile; viewerInfo.inputPlayer = localUserViewer?.inputPlayer; viewerInfo.eventSystem = localUserViewer?.eventSystem; viewerInfo.hasCursor = (bool)viewerInfo.eventSystem && viewerInfo.eventSystem.isCursorVisible; viewerInfo.isUIFocused = localUserViewer?.isUIFocused ?? false; } public void CloneCameraContext(CameraRigController cloneThisCamera) { if (!(cloneThisCamera == null)) { cameraModeContext = cloneThisCamera.cameraModeContext; } } public float Raycast(Ray ray, float maxDistance, float wallCushion) { int num = 0; num = HGPhysics.SphereCastAll(out var hits, ray.origin, wallCushion, ray.direction, maxDistance, LayerIndex.world.mask, QueryTriggerInteraction.Ignore); float num2 = maxDistance; for (int i = 0; i < num; i++) { float distance = hits[i].distance; if (distance < num2) { Collider collider = hits[i].collider; if ((bool)collider && !collider.GetComponent()) { num2 = distance; } } } HGPhysics.ReturnResults(hits); return num2; } private static float RemapLerpTime(float t) { float num = 1f; float num2 = 0f; float num3 = 1f; if ((t /= num / 2f) < 1f) { return num3 / 2f * t * t + num2; } return (0f - num3) / 2f * ((t -= 1f) * (t - 2f) - 1f) + num2; } public void SetCameraState(CameraState cameraState) { currentCameraState = cameraState; float num = ((localUserViewer == null) ? 1f : localUserViewer.userProfile.screenShakeScale); Vector3 position = cameraState.position; rawScreenShakeDisplacement = ShakeEmitter.ComputeTotalShakeAtPoint(cameraState.position); Vector3 vector = rawScreenShakeDisplacement * num; Vector3 position2 = position + vector; if (vector != Vector3.zero && Physics.SphereCast(position, direction: vector, radius: sceneCam.nearClipPlane, hitInfo: out var hitInfo, maxDistance: vector.magnitude, layerMask: LayerIndex.world.mask, queryTriggerInteraction: QueryTriggerInteraction.Ignore)) { position2 = position + vector.normalized * hitInfo.distance; } base.transform.SetPositionAndRotation(position2, cameraState.rotation); if ((bool)sceneCam) { sceneCam.fieldOfView = cameraState.fov; } } public static Ray ModifyAimRayIfApplicable(Ray originalAimRay, GameObject target, out float extraRaycastDistance) { CameraRigController cameraRigController = null; for (int i = 0; i < readOnlyInstancesList.Count; i++) { CameraRigController cameraRigController2 = readOnlyInstancesList[i]; if (cameraRigController2.target == target && cameraRigController2._localUserViewer.cachedBodyObject == target && !cameraRigController2.hasOverride) { cameraRigController = cameraRigController2; break; } } if ((bool)cameraRigController) { Camera camera = cameraRigController.sceneCam; extraRaycastDistance = (originalAimRay.origin - camera.transform.position).magnitude; return camera.ViewportPointToRay(new Vector2(0.5f, 0.5f)); } extraRaycastDistance = 0f; return originalAimRay; } private void SetSprintParticlesActive(bool newSprintParticlesActive) { if (!sprintingParticleSystem) { return; } ParticleSystem.MainModule main = sprintingParticleSystem.main; if (newSprintParticlesActive) { main.loop = true; if (!sprintingParticleSystem.isPlaying) { sprintingParticleSystem.Play(); } } else { main.loop = false; } } private void SetParticleSystemActive(bool newParticlesActive, ParticleSystem particleSystem) { if (!particleSystem) { return; } ParticleSystem.MainModule main = particleSystem.main; if (newParticlesActive) { main.loop = true; if (!particleSystem.isPlaying) { particleSystem.Play(); } } else { main.loop = false; } } public void SetPostProcessVolumeExclusive(PostProcessVolume volume) { if (!(volume == null) && !(sceneCam == null)) { volume.targetLayer = sceneCam.GetComponent(); } } [InitDuringStartup] private static void Init() { SceneCamera.onSceneCameraPreCull += delegate(SceneCamera sceneCam) { sceneCam.cameraRigController.sprintingParticleSystem.gameObject.layer = LayerIndex.defaultLayer.intVal; }; SceneCamera.onSceneCameraPostRender += delegate(SceneCamera sceneCam) { sceneCam.cameraRigController.sprintingParticleSystem.gameObject.layer = LayerIndex.noDraw.intVal; }; } public static bool IsObjectSpectatedByAnyCamera(GameObject gameObject) { for (int i = 0; i < instancesList.Count; i++) { if (instancesList[i].target == gameObject) { return true; } } return false; } [ContextMenu("Print Debug Info")] private void PrintDebugInfo() { Debug.LogFormat("hasOverride={0} overrideCam={1} isControlAllowed={2}", hasOverride, overrideCam, isControlAllowed); } public void EnableMusicForCamera(string deviceShareSet = "Music_Audio_Device") { GameObject gameObject = new GameObject("MusicListener", typeof(AkGameObj), typeof(AkAudioListener)); Transform obj = gameObject.transform; obj.localPosition = Vector3.zero; obj.localRotation = Quaternion.identity; obj.localScale = Vector3.one; obj.SetParent(sceneCam.transform, worldPositionStays: false); AkOutputSettings in_Settings = new AkOutputSettings(deviceShareSet); ulong[] array = new ulong[1] { AkSoundEngine.GetAkGameObjectID(gameObject) }; AkSoundEngine.AddOutput(in_Settings, out var out_pDeviceID, array, (uint)array.Length); musicListenerId = out_pDeviceID; musicEnabled = true; } public void DisableMusicForCamera() { if (musicEnabled) { AkSoundEngine.RemoveOutput(musicListenerId); } } }