diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1c59b9b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +Changelog: + +Enable TargetLock when mounted on a dragon: +------------------------------------------ + + Hooks.cpp and Hooks.h + * New class DragonCameraState, same functions and members as HorseCameraState, but for the dragon camerastate + * New hook DragonCameraStateHook (same functions as HorseCameraStateHook, but hooking into the DragonCameraState) + * Renamed SaveCamera::RotationType::kHorse to SaveCamera::RotationType::kMount to reflect both horse and dragon mounts (no functional changes) + + DirectionalMovementHandler.cpp and Hooks.cpp + * Added checks for RE::CameraStates::kDragon whererever there are checks for RE::CameraState::kMount + * Use DragonCameraState instead of HorseCameraState in case camera state is RE::CameraState::kDragon + + DirectionalMovementHandler.cpp + * modified handling of camera snap in ToggleTargetLock() and LookAtTarget() such that DragonCameraState->dragonRefHandle.get()->As()->data.angle.x is not modified. + ( otherwise if target lock is on, the dragon's orientation flickers between two positions while the dragon is switching flying states (flying->hover, or hover->land) + * In ToggleTargetLock(), when toggling the TargetLock on, the target lock is set to the dragon's current combat target instead of using the actor which is closest to the center of the screen + (only in case IDRC is active and the dragon is mounted and in combat). This functionality uses IDRC's API function APIs::IDRC->GetCurrentTarget(). It is used if APIs::IDRC->UseTarget() returns true. + * In LookAtTarget(): + - use dragon instead of player as reference because player's yaw is changing with dragon's head orientation. + + - enhanced the handling of the situation when the target is behind the camera (ie camera is between player and target): + - added distance check to bIsBehind to avoid that the camera is rotating towards the player-axis in case the target is further away from the player as the camera + (in that case, the target ended up behind the camera). + - The distance check also addresses this case: If the target is close behind the camera there was a tendency for camera oscillation between two positions close to the player-target axis. Reason is that projectedDirectionToTargetXY changes significantly through the camera movement because the distance btw camera and target is short. That in turn leads to angleDelta values switching sign between frames. + + * IDRC API is added via API/IDRC_API.h, and connected in API/APIManager.cpp and API/APIManager.h + * IDRC API requires unreleased version of IDRC (https://github.com/staalo18/IntuitiveDragonRideControl) + + +Changed handling of reference pitch for horseCameraState and dragonCameraState: +------------------------------------------------------------------------------- + * DirectionalMovementHandler.cpp: in LookAtTarget(), use referencePitch = 0 for horse and dragon camera states, instead of _desiredPlayerPitch. + + +New option: TargetLock - Min height above ground: +------------------------------------------------- + * Introduced functionality to keep the camera above mininmal height over ground. Two new functions: + * DirectionalMovementHandler.cpp: GetCameraAngle() - Checks for ground level and adapts angle if camera would move below min height. + * Utils.cpp: GetLandHeightWithWater() - provides z-coord of land height, considering water level. + * New MCM option (in TargetLock category): "Min Camera Height Above Ground", with default 35. This required changes in: + MCM/Config/TrueDirectionalMovement/config.json + MCM/Config/TrueDirectionalMovement/settings.ini + Translation/TrueDirectionalMovement_english.txt + Settings.cpp + Settings.h + * In Settings.h: bTargetLockConsiderGroundLevel (true) - switch to turn off Min-height-above-ground functionality + +New option: TargetLock - Lock behind target: +-------------------------------------------- + * DirectionalMovementHandler: + * new functions: + EnableLockBehindTarget) + ToggleLockBehindTarget() + GetNominalCameraToPlayerDistance() + GetNominalCameraPosition() + UpdateMoveCameraBehindTarget() + * LookAtTarget(): Modifications to compute yaw and pitch for the "lock-behind-target" case (probably more bad math ahead ...). + * Utils.cpp: new function GetFlyingState() + * Settings.h: fCameraBehindTargetMinDistance, fCameraBehindTargetNoSwitchRange (not in MCM) + * New MCM options (in TargetLock category): "Enable Lock Behind Target" (default: off), and "Toggle Lock-Behind-Target Key" (default: undefined). This required changes in: + MCM/Config/TrueDirectionalMovement/config.json + MCM/Config/TrueDirectionalMovement/settings.ini + Translation/TrueDirectionalMovement_english.txt + Settings.cpp + Settings.h + Events.cpp (in InputEventHandler::ProcessEvent() - check for Toggle Lock-Behind-Target Key) diff --git a/MCM/Config/TrueDirectionalMovement/config.json b/MCM/Config/TrueDirectionalMovement/config.json new file mode 100644 index 0000000..919d366 --- /dev/null +++ b/MCM/Config/TrueDirectionalMovement/config.json @@ -0,0 +1,1344 @@ +{ + "modName": "TrueDirectionalMovement", + "displayName": "True Directional Movement", + "minMcmVersion": 11, + "cursorFillMode": "topToBottom", + "pages": [{ + "pageDisplayName": "$TrueDirectionalMovement_DirectionalMovementPage", + "cursorFillMode": "topToBottom", + "content": [ + { + "text": "$TrueDirectionalMovement_DirectionalMovement_HeaderText", + "type": "header" + }, + { + "id": "uDirectionalMovementSheathed:DirectionalMovement", + "text": "$TrueDirectionalMovement_DirectionalMovementSheathed_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_DirectionalMovementSheathed_InfoText", + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_DirectionalMovementMode_DisabledNotRecommended", + "$TrueDirectionalMovement_DirectionalMovementMode_VanillaStyle", + "$TrueDirectionalMovement_DirectionalMovementMode_Directional" + ], + "shortNames": [ + "$TrueDirectionalMovement_DirectionalMovementMode_Disabled", + "$TrueDirectionalMovement_DirectionalMovementMode_Vanilla", + "$TrueDirectionalMovement_DirectionalMovementMode_Directional" + ], + "sourceType": "ModSettingInt" + } + }, + { + "id": "uDirectionalMovementDrawn:DirectionalMovement", + "text": "$TrueDirectionalMovement_DirectionalMovementDrawn_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_DirectionalMovementDrawn_InfoText", + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_DirectionalMovementMode_DisabledNotRecommended", + "$TrueDirectionalMovement_DirectionalMovementMode_VanillaStyle", + "$TrueDirectionalMovement_DirectionalMovementMode_Directional" + ], + "shortNames": [ + "$TrueDirectionalMovement_DirectionalMovementMode_Disabled", + "$TrueDirectionalMovement_DirectionalMovementMode_Vanilla", + "$TrueDirectionalMovement_DirectionalMovementMode_Directional" + ], + "sourceType": "ModSettingInt" + } + }, + { + "id": "uDialogueMode:DirectionalMovement", + "text": "$TrueDirectionalMovement_DialogueMode_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_DialogueMode_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_DialogueMode_Disable", + "$TrueDirectionalMovement_DialogueMode_Normal", + "$TrueDirectionalMovement_DialogueMode_FaceSpeaker" + ], + "sourceType": "ModSettingInt" + } + }, + { + "id": "fMeleeMagnetismAngle:DirectionalMovement", + "text": "$TrueDirectionalMovement_MeleeMagnetismAngle_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_MeleeMagnetismAngle_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0, + "max": 180, + "step": 5, + "sourceType": "ModSettingFloat" + } + }, + { + "id": "bMagnetismWhileBlocking:DirectionalMovement", + "text": "$TrueDirectionalMovement_MagnetismWhileBlocking_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_MagnetismWhileBlocking_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bFaceCrosshairWhileAttacking:DirectionalMovement", + "text": "$TrueDirectionalMovement_FaceCrosshairWhileAttacking_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_FaceCrosshairWhileAttacking_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bFaceCrosshairWhileShouting:DirectionalMovement", + "text": "$TrueDirectionalMovement_FaceCrosshairWhileShouting_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_FaceCrosshairWhileShouting_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bFaceCrosshairWhileBlocking:DirectionalMovement", + "text": "$TrueDirectionalMovement_FaceCrosshairWhileBlocking_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_FaceCrosshairWhileBlocking_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bFaceCrosshairDuringAutoMove:DirectionalMovement", + "text": "$TrueDirectionalMovement_FaceCrosshairDuringAutoMove_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_FaceCrosshairDuringAutoMove_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bStopOnDirectionChange:DirectionalMovement", + "text": "$TrueDirectionalMovement_StopOnDirectionChange_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_StopOnDirectionChange_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "uAdjustCameraYawDuringMovement:DirectionalMovement", + "text": "$TrueDirectionalMovement_AdjustCameraYawDuringMovement_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_AdjustCameraYawDuringMovement_InfoText", + "groupCondition": [1, 2], + "groupControl": 6, + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_CameraAdjustMode_Disable", + "$TrueDirectionalMovement_CameraAdjustMode_DuringSprint", + "$TrueDirectionalMovement_CameraAdjustMode_Always" + ], + "shortNames": [ + "$TrueDirectionalMovement_CameraAdjustMode_Disable", + "$TrueDirectionalMovement_CameraAdjustMode_Sprint", + "$TrueDirectionalMovement_CameraAdjustMode_Moving" + ], + "sourceType": "ModSettingInt" + } + }, + { + "id": "fCameraAutoAdjustDelay:DirectionalMovement", + "text": "$TrueDirectionalMovement_CameraAutoAdjustDelay_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_CameraAutoAdjustDelay_InfoText", + "groupCondition": {"AND": [6, {"OR": [1, 2]}]}, + "valueOptions": { + "min": 0.0, + "max": 5.0, + "step": 0.1, + "formatString": "{1} s", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fCameraAutoAdjustSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_CameraAutoAdjustSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_CameraAutoAdjustSpeedMult_InfoText", + "groupCondition": {"AND": [6, {"OR": [1, 2]}]}, + "valueOptions": { + "min": 0.1, + "max": 4.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fControllerBufferDepth:DirectionalMovement", + "text": "$TrueDirectionalMovement_ControllerBufferDepth_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_ControllerBufferDepth_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.01, + "max": 0.20, + "step": 0.01, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + + { + "text": "$TrueDirectionalMovement_Multipliers_HeaderText", + "type": "header", + "position": 1 + }, + { + "id": "fRunningRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_RunningRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_RunningRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.1, + "max": 5.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fSprintingRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_SprintingRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_SprintingRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.1, + "max": 5.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fAttackStartRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_AttackStartRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_AttackStartRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.0, + "max": 20.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fAttackMidRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_AttackMidRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_AttackMidRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.0, + "max": 4.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fAttackEndRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_AttackEndRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_AttackEndRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.0, + "max": 4.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fAirRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_AirRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_AirRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.0, + "max": 1.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fGlidingRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_GlidingRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_GlidingRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.0, + "max": 1.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fWaterRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_WaterRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_WaterRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.1, + "max": 1.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fSwimmingRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_SwimmingRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_SwimmingRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.1, + "max": 2.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fDodgeUnlockedRotationSpeedMult:DirectionalMovement", + "text": "$TrueDirectionalMovement_DodgeUnlockedRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_DodgeUnlockedRotationSpeedMult_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.1, + "max": 2.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fFaceCrosshairRotationSpeedMultiplier:DirectionalMovement", + "text": "$TrueDirectionalMovement_FaceCrosshairRotationSpeedMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_FaceCrosshairRotationSpeedMult_InfoText", + "groupCondition": {"AND": [{"NOT": 5}, {"OR": [1, 2]}]}, + "valueOptions": { + "min": 0.1, + "max": 5.0, + "step": 0.05, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "bFaceCrosshairInstantly:DirectionalMovement", + "text": "$TrueDirectionalMovement_FaceCrosshairInstantly_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_FaceCrosshairInstantly_InfoText", + "groupCondition": [1, 2], + "groupControl": 5, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bIgnoreSlowTime:DirectionalMovement", + "text": "$TrueDirectionalMovement_IgnoreSlowTime_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_IgnoreSlowTime_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bDisableAttackRotationMultipliersForTransformations:DirectionalMovement", + "text": "$TrueDirectionalMovement_DisableAttackRotationMultipliersForTransformations_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_DisableAttackRotationMultipliersForTransformations_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fSwimmingPitchSpeed:DirectionalMovement", + "text": "$TrueDirectionalMovement_SwimmingPitchSpeed_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_SwimmingPitchSpeed_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 1.0, + "max": 10.0, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + } + ] + }, + { + "pageDisplayName": "$TrueDirectionalMovement_LeaningPage", + "cursorFillMode": "topToBottom", + "content": [ + { + "text": "$TrueDirectionalMovement_Leaning_HeaderText", + "type": "header" + }, + { + "id": "bEnableLeaning:Leaning", + "text": "$TrueDirectionalMovement_EnableLeaning_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_EnableLeaning_InfoText", + "groupControl": 1, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bEnableLeaningNPC:Leaning", + "text": "$TrueDirectionalMovement_EnableLeaningNPC_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_EnableLeaningNPC_InfoText", + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fLeaningMult:Leaning", + "text": "$TrueDirectionalMovement_LeaningMult_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_LeaningMult_InfoText", + "groupCondition": 1, + "valueOptions": { + "min": 0.1, + "max": 10.0, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fLeaningSpeed:Leaning", + "text": "$TrueDirectionalMovement_LeaningSpeed_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_LeaningSpeed_InfoText", + "groupCondition": 1, + "valueOptions": { + "min": 0.1, + "max": 10.0, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fMaxLeaningStrength:Leaning", + "text": "$TrueDirectionalMovement_MaxLeaningStrength_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_MaxLeaningStrength_InfoText", + "groupCondition": 1, + "valueOptions": { + "min": 0.1, + "max": 100.0, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "text": "$TrueDirectionalMovement_Nemesis_HeaderText", + "type": "header", + "position": 1 + }, + { + "type": "hiddenToggle", + "groupControl": 2, + "valueOptions": { + "sourceType": "GlobalValue", + "sourceForm": "TrueDirectionalMovement.esp|813" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisLeaning_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisLeaningInstalled_InfoText", + "groupCondition": 2, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisLeaningInstalled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisLeaning_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisLeaningNotInstalled_InfoText", + "groupCondition": {"NOT": 2}, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisLeaningNotInstalled_ValueText" + } + } + ] + }, + { + "pageDisplayName": "$TrueDirectionalMovement_HeadtrackingPage", + "cursorFillMode": "topToBottom", + "content": [ + { + "text": "$TrueDirectionalMovement_Headtracking_HeaderText", + "type": "header" + }, + { + "id": "bHeadtracking:Headtracking", + "text": "$TrueDirectionalMovement_Headtracking_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_Headtracking_InfoText", + "groupControl": 1, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bHeadtrackSpine:Headtracking", + "text": "$TrueDirectionalMovement_HeadtrackSpine_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_HeadtrackSpine_InfoText", + "groupCondition": 1, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fDialogueHeadtrackingDuration:Headtracking", + "text": "$TrueDirectionalMovement_DialogueHeadtrackingDuration_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_DialogueHeadtrackingDuration_InfoText", + "groupCondition": 1, + "valueOptions": { + "min": 0.5, + "max": 10.0, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "bCameraHeadtracking:Headtracking", + "text": "$TrueDirectionalMovement_CameraHeadtracking_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_CameraHeadtracking_InfoText", + "groupCondition": 1, + "groupControl": 2, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fCameraHeadtrackingStrength:Headtracking", + "text": "$TrueDirectionalMovement_CameraHeadtrackingStrength_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_CameraHeadtrackingStrength_InfoText", + "groupCondition": {"AND": [1, 2]}, + "valueOptions": { + "min": 0.1, + "max": 2.0, + "step": 0.01, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fCameraHeadtrackingDuration:Headtracking", + "text": "$TrueDirectionalMovement_CameraHeadtrackingDuration_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_CameraHeadtrackingDuration_InfoText", + "groupCondition": {"AND": [1, 2]}, + "valueOptions": { + "min": 0.0, + "max": 10.0, + "step": 0.1, + "formatString": "{1} s", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "uCameraHeadtrackingMode:Headtracking", + "text": "$TrueDirectionalMovement_CameraHeadtrackingMode_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_CameraHeadtrackingMode_InfoText", + "groupCondition": {"AND": [1, 2]}, + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_CameraHeadtrackingMode_Disable", + "$TrueDirectionalMovement_CameraHeadtrackingMode_Normal", + "$TrueDirectionalMovement_CameraHeadtrackingMode_FaceCamera" + ], + "sourceType": "ModSettingInt" + } + }, + { + "text": "$TrueDirectionalMovement_Nemesis_HeaderText", + "type": "header", + "position": 1 + }, + { + "type": "hiddenToggle", + "groupControl": 3, + "valueOptions": { + "sourceType": "GlobalValue", + "sourceForm": "TrueDirectionalMovement.esp|811" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisHeadtracking_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisHeadtrackingInstalled_InfoText", + "groupCondition": 3, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisHeadtrackingInstalled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisHeadtracking_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisHeadtrackingNotInstalled_InfoText", + "groupCondition": {"NOT": 3}, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisHeadtrackingNotInstalled_ValueText" + } + } + ] + }, + { + "pageDisplayName": "$TrueDirectionalMovement_TargetLockPage", + "cursorFillMode": "topToBottom", + "content": [ + { + "text": "$TrueDirectionalMovement_TargetLockSettings_HeaderText", + "type": "header" + }, + { + "id": "bAutoTargetNextOnDeath:TargetLock", + "text": "$TrueDirectionalMovement_AutoTargetNextOnDeath_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_AutoTargetNextOnDeath_InfoText", + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bTargetLockTestLOS:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockTestLOS_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockTestLOS_InfoText", + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bTargetLockHostileActorsOnly:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockHostileActorsOnly_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockHostileActorsOnly_InfoText", + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bTargetLockHideCrosshair:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockHideCrosshair_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockHideCrosshair_InfoText", + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fTargetLockDistance:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockDistance_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockDistance_InfoText", + "valueOptions": { + "min": 100, + "max": 8000, + "step": 10, + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fTargetLockDistanceMultiplierSmall:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockDistanceMultiplierSmall_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockDistanceMultiplierSmall_InfoText", + "valueOptions": { + "min": 0, + "max": 10, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fTargetLockDistanceMultiplierLarge:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockDistanceMultiplierLarge_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockDistanceMultiplierLarge_InfoText", + "valueOptions": { + "min": 0, + "max": 10, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fTargetLockDistanceMultiplierExtraLarge:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockDistanceMultiplierExtraLarge_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockDistanceMultiplierExtraLarge_InfoText", + "valueOptions": { + "min": 0, + "max": 10, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "bTargetLockEnableLockBehindTarget:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockEnableLockBehindTarget_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockEnableLockBehindTarget_InfoText", + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + + { + "text": "$TrueDirectionalMovement_CameraSettings_HeaderText", + "type": "header" + }, + { + "id": "fTargetLockPitchAdjustSpeed:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockPitchAdjustSpeed_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockPitchAdjustSpeed_InfoText", + "valueOptions": { + "min": 0.1, + "max": 20.0, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fTargetLockYawAdjustSpeed:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockYawAdjustSpeed_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockYawAdjustSpeed_InfoText", + "valueOptions": { + "min": 0.1, + "max": 40.0, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fTargetLockPitchOffsetStrength:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockPitchOffsetStrength_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockPitchOffsetStrength_InfoText", + "valueOptions": { + "min": 0.0, + "max": 1.0, + "step": 0.01, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fTargetLockMinHeightAboveGround:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockMinHeightAboveGround_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockMinHeightAboveGround_InfoText", + "valueOptions": { + "min": 0, + "max": 200, + "step": 5, + "sourceType": "ModSettingFloat" + } + }, + + { + "text": "$TrueDirectionalMovement_ProjectileSettings_HeaderText", + "type": "header" + }, + { + "id": "uTargetLockArrowAimType:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockArrowAimType_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_TargetLockArrowAimType_InfoText", + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_TargetLockProjectileAimType_FreeAim", + "$TrueDirectionalMovement_TargetLockProjectileAimType_Predict", + "$TrueDirectionalMovement_TargetLockProjectileAimType_Homing" + ], + "sourceType": "ModSettingInt" + } + }, + { + "id": "uTargetLockMissileAimType:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockMissileAimType_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_TargetLockMissileAimType_InfoText", + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_TargetLockProjectileAimType_FreeAim", + "$TrueDirectionalMovement_TargetLockProjectileAimType_Predict", + "$TrueDirectionalMovement_TargetLockProjectileAimType_Homing" + ], + "sourceType": "ModSettingInt" + } + }, + + { + "text": "$TrueDirectionalMovement_Controls_HeaderText", + "type": "header", + "position": 1 + }, + { + "id": "uTargetLockKey:Keys", + "text": "$TrueDirectionalMovement_TargetLockKey_OptionText", + "type": "keymap", + "help": "$TrueDirectionalMovement_TargetLockKey_InfoText", + "ignoreConflicts": false, + "valueOptions": { + "sourceType": "ModSettingInt" + } + }, + { + "id": "bTargetLockUsePOVSwitchKeyboard:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockUsePOVSwitchKeyboard_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockUsePOVSwitchKeyboard_InfoText", + "groupControl": 1, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bTargetLockUsePOVSwitchGamepad:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockUsePOVSwitchGamepad_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockUsePOVSwitchGamepad_InfoText", + "groupControl": 2, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fTargetLockPOVHoldDuration:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockPOVHoldDuration_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockPOVHoldDuration_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "min": 0.0, + "max": 1.0, + "step": 0.01, + "formatString": "{2} s", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "bResetCameraWithTargetLock:TargetLock", + "text": "$TrueDirectionalMovement_ResetCameraWithTargetLock_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_ResetCameraWithTargetLock_InfoText", + "groupControl": 3, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bResetCameraPitch:TargetLock", + "text": "$TrueDirectionalMovement_ResetCameraPitch_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_ResetCameraPitch_InfoText", + "groupCondition": 3, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bTargetLockUseMouse:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockUseMouse_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockUseMouse_InfoText", + "groupControl": 4, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "uTargetLockMouseSensitivity:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockMouseSensitivity_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_TargetLockMouseSensitivity_InfoText", + "groupCondition": 4, + "valueOptions": { + "min": 1, + "max": 200, + "step": 1, + "sourceType": "ModSettingInt" + } + }, + { + "id": "bTargetLockUseScrollWheel:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockUseScrollWheel_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockUseScrollWheel_InfoText", + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "bTargetLockUseRightThumbstick:TargetLock", + "text": "$TrueDirectionalMovement_TargetLockUseRightThumbstick_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_TargetLockUseRightThumbstick_InfoText", + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + + { + "text": "$TrueDirectionalMovement_OptionalControls_HeaderText", + "type": "header" + }, + { + "id": "uSwitchTargetLeftKey:Keys", + "text": "$TrueDirectionalMovement_SwitchTargetLeftKey_OptionText", + "type": "keymap", + "help": "$TrueDirectionalMovement_SwitchTargetLeftKey_InfoText", + "ignoreConflicts": false, + "valueOptions": { + "sourceType": "ModSettingInt" + } + }, + { + "id": "uSwitchTargetRightKey:Keys", + "text": "$TrueDirectionalMovement_SwitchTargetRightKey_OptionText", + "type": "keymap", + "help": "$TrueDirectionalMovement_SwitchTargetRightKey_InfoText", + "ignoreConflicts": false, + "valueOptions": { + "sourceType": "ModSettingInt" + } + }, + { + "id": "uTargetLockBehindTargetKey:Keys", + "text": "$TrueDirectionalMovement_TargetLockBehindTargetKey_OptionText", + "type": "keymap", + "help": "$TrueDirectionalMovement_TargetLockBehindTargetKey_InfoText", + "ignoreConflicts": false, + "valueOptions": { + "sourceType": "ModSettingInt" + } + } + ] + }, + { + "pageDisplayName": "$TrueDirectionalMovement_HUDPage", + "cursorFillMode": "topToBottom", + "content": [ + { + "type": "hiddenToggle", + "groupControl": 1, + "valueOptions": { + "sourceType": "GlobalValue", + "sourceForm": "TrueDirectionalMovement.esp|810" + } + }, + { + "text": "$TrueDirectionalMovement_HUD_HeaderText", + "type": "header" + }, + { + "id": "bEnableTargetLockReticle:HUD", + "text": "$TrueDirectionalMovement_EnableTargetLockReticle_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_EnableTargetLockReticle_InfoText", + "groupCondition": 1, + "groupControl": 2, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "text": "$TrueDirectionalMovement_ReticleSettings_HeaderText", + "type": "header" + }, + { + "id": "uReticleAnchor:HUD", + "text": "$TrueDirectionalMovement_ReticleAnchor_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_ReticleAnchor_InfoText", + "groupCondition": {"AND": [1, 2]}, + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_WidgetAnchor_Body", + "$TrueDirectionalMovement_WidgetAnchor_Head" + ], + "sourceType": "ModSettingInt" + } + }, + { + "id": "uReticleStyle:HUD", + "text": "$TrueDirectionalMovement_ReticleStyle_OptionText", + "type": "enum", + "help": "$TrueDirectionalMovement_ReticleStyle_InfoText", + "groupCondition": {"AND": [1, 2]}, + "valueOptions": { + "options": [ + "$TrueDirectionalMovement_ReticleStyle_Crosshair", + "$TrueDirectionalMovement_ReticleStyle_CrosshairNoTransform", + "$TrueDirectionalMovement_ReticleStyle_Dot", + "$TrueDirectionalMovement_ReticleStyle_Glow" + ], + "shortNames": [ + "$TrueDirectionalMovement_ReticleStyle_Crosshair", + "$TrueDirectionalMovement_ReticleStyle_CrosshairSimpler", + "$TrueDirectionalMovement_ReticleStyle_Dot", + "$TrueDirectionalMovement_ReticleStyle_Glow" + ], + "sourceType": "ModSettingInt" + } + }, + { + "id": "fReticleScale:HUD", + "text": "$TrueDirectionalMovement_ReticleScale_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_ReticleScale_InfoText", + "groupCondition": {"AND": [1, 2]}, + "valueOptions": { + "min": 0.1, + "max": 2.0, + "step": 0.1, + "formatString": "{1}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "bReticleUseHUDOpacity:HUD", + "text": "$TrueDirectionalMovement_ReticleUseHUDOpacity_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_ReticleUseHUDOpacity_InfoText", + "groupCondition": {"AND": [1, 2]}, + "groupControl": 3, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fReticleOpacity:HUD", + "text": "$TrueDirectionalMovement_ReticleOpacity_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_ReticleOpacity_InfoText", + "groupCondition": {"AND": [{"AND": [1, 2]}, {"NOT": 3}]}, + "valueOptions": { + "min": 0.01, + "max": 1.0, + "step": 0.01, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "text": "$TrueDirectionalMovement_TrueHUD_HeaderText", + "type": "header", + "position": 1 + }, + { + "text": "$TrueDirectionalMovement_TrueHUD_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_TrueHUDEnabled_InfoText", + "groupCondition": 1, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_TrueHUDEnabled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_TrueHUD_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_TrueHUDDisabled_InfoText", + "groupCondition": {"NOT": 1}, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_TrueHUDDisabled_ValueText" + } + } + ] + }, + { + "pageDisplayName": "$TrueDirectionalMovement_MiscPage", + "cursorFillMode": "topToBottom", + "content": [ + { + "text": "$TrueDirectionalMovement_Misc_HeaderText", + "type": "header" + }, + { + "id": "bOverrideAcrobatics:Misc", + "text": "$TrueDirectionalMovement_OverrideAcrobatics_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_OverrideAcrobatics_InfoText", + "groupControl": 4, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fAcrobatics:Misc", + "text": "$TrueDirectionalMovement_Acrobatics_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_Acrobatics_InfoText", + "groupCondition": 4, + "valueOptions": { + "min": 0.0, + "max": 1.0, + "step": 0.001, + "formatString": "{3}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fAcrobaticsGliding:Misc", + "text": "$TrueDirectionalMovement_AcrobaticsGliding_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_AcrobaticsGliding_InfoText", + "groupCondition": 4, + "valueOptions": { + "min": 0.0, + "max": 1.0, + "step": 0.001, + "formatString": "{3}", + "sourceType": "ModSettingFloat" + } + }, + { + "text": "$TrueDirectionalMovement_Controller_HeaderText", + "type": "header" + }, + { + "id": "bOverrideControllerDeadzone:Controller", + "text": "$TrueDirectionalMovement_OverrideControllerDeadzone_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_OverrideControllerDeadzone_InfoText", + "groupControl": 3, + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + { + "id": "fControllerRadialDeadzone:Controller", + "text": "$TrueDirectionalMovement_ControllerRadialDeadzone_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_ControllerRadialDeadzone_InfoText", + "groupCondition": 3, + "valueOptions": { + "min": 0.0, + "max": 0.5, + "step": 0.01, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "fControllerAxialDeadzone:Controller", + "text": "$TrueDirectionalMovement_ControllerAxialDeadzone_OptionText", + "type": "slider", + "help": "$TrueDirectionalMovement_ControllerAxialDeadzone_InfoText", + "groupCondition": 3, + "valueOptions": { + "min": 0.0, + "max": 0.5, + "step": 0.01, + "formatString": "{2}", + "sourceType": "ModSettingFloat" + } + }, + { + "id": "bThumbstickBounceFix:Controller", + "text": "$TrueDirectionalMovement_ThumbstickBounceFix_OptionText", + "type": "toggle", + "help": "$TrueDirectionalMovement_ThumbstickBounceFix_InfoText", + "groupCondition": [1, 2], + "valueOptions": { + "sourceType": "ModSettingBool" + } + }, + + { + "text": "$TrueDirectionalMovement_Nemesis_HeaderText", + "type": "header", + "position": 1 + }, + { + "type": "hiddenToggle", + "groupControl": 4, + "valueOptions": { + "sourceType": "GlobalValue", + "sourceForm": "TrueDirectionalMovement.esp|811" + } + }, + { + "type": "hiddenToggle", + "groupControl": 5, + "valueOptions": { + "sourceType": "GlobalValue", + "sourceForm": "TrueDirectionalMovement.esp|812" + } + }, + { + "type": "hiddenToggle", + "groupControl": 6, + "valueOptions": { + "sourceType": "GlobalValue", + "sourceForm": "TrueDirectionalMovement.esp|813" + } + }, + { + "type": "hiddenToggle", + "groupControl": 7, + "valueOptions": { + "sourceType": "GlobalValue", + "sourceForm": "TrueDirectionalMovement.esp|810" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisHeadtracking_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisHeadtrackingInstalled_InfoText", + "groupCondition": 4, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisHeadtrackingInstalled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisHeadtracking_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisHeadtrackingNotInstalled_InfoText", + "groupCondition": {"NOT": 4}, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisHeadtrackingNotInstalled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisMountedArchery_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisMountedArcheryInstalled_InfoText", + "groupCondition": 5, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisMountedArcheryInstalled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisMountedArchery_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisMountedArcheryNotInstalled_InfoText", + "groupCondition": {"NOT": 5}, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisMountedArcheryNotInstalled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisLeaning_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisLeaningInstalled_InfoText", + "groupCondition": 6, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisLeaningInstalled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_NemesisLeaning_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_NemesisLeaningNotInstalled_InfoText", + "groupCondition": {"NOT": 6}, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_NemesisLeaningNotInstalled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_TrueHUD_HeaderText", + "type": "header" + }, + { + "text": "$TrueDirectionalMovement_TrueHUD_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_TrueHUDEnabled_InfoText", + "groupCondition": 7, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_TrueHUDEnabled_ValueText" + } + }, + { + "text": "$TrueDirectionalMovement_TrueHUD_OptionText", + "type": "text", + "help": "$TrueDirectionalMovement_TrueHUDDisabled_InfoText", + "groupCondition": {"NOT": 7}, + "groupBehavior": "skip", + "valueOptions": { + "value": "$TrueDirectionalMovement_TrueHUDDisabled_ValueText" + } + } + ] + }], + "customContent": { + "source": "TrueDirectionalMovement/TDM_Splash.swf" + } +} \ No newline at end of file diff --git a/MCM/Config/TrueDirectionalMovement/settings.ini b/MCM/Config/TrueDirectionalMovement/settings.ini new file mode 100644 index 0000000..0484fc3 --- /dev/null +++ b/MCM/Config/TrueDirectionalMovement/settings.ini @@ -0,0 +1,97 @@ +[DirectionalMovement] +uDirectionalMovementSheathed = 2 +uDirectionalMovementDrawn = 2 +uDialogueMode = 2 +fMeleeMagnetismAngle = 60 +bMagnetismWhileBlocking = 1 +bFaceCrosshairWhileAttacking = 0 +bFaceCrosshairWhileShouting = 0 +bFaceCrosshairWhileBlocking = 1 +bFaceCrosshairDuringAutoMove = 1 +bStopOnDirectionChange = 1 +uAdjustCameraYawDuringMovement = 1 +fRunningRotationSpeedMult = 1.5 +fSprintingRotationSpeedMult = 2 +fAttackStartRotationSpeedMult = 5 +fAttackMidRotationSpeedMult = 1 +fAttackEndRotationSpeedMult = 0 +fAirRotationSpeedMult = 0.25 +fGlidingRotationSpeedMult = 0.5 +fWaterRotationSpeedMult = 0.5 +fSwimmingRotationSpeedMult = 2 +fDodgeUnlockedRotationSpeedMult = 0.5 +fFaceCrosshairRotationSpeedMultiplier = 2 +bFaceCrosshairInstantly = 0 +fCameraAutoAdjustDelay = 0.1 +fCameraAutoAdjustSpeedMult = 1.5 +bIgnoreSlowTime = 0 +bDisableAttackRotationMultipliersForTransformations = 1 +fSwimmingPitchSpeed = 3 +fControllerBufferDepth = 0.02 + +[Leaning] +bEnableLeaning = 1 +bEnableLeaningNPC = 1 +fLeaningMult = 2 +fLeaningSpeed = 4 +fMaxLeaningStrength = 10 + +[Headtracking] +bHeadtracking = 1 +bHeadtrackSpine = 1 +fDialogueHeadtrackingDuration = 3 +bCameraHeadtracking = 1 +fCameraHeadtrackingStrength = 0.75 +fCameraHeadtrackingDuration = 1 +uCameraHeadtrackingMode = 0 + +[TargetLock] +bAutoTargetNextOnDeath = 1 +bTargetLockTestLOS = 1 +bTargetLockHostileActorsOnly = 1 +bTargetLockHideCrosshair = 1 +fTargetLockDistance = 2000 +fTargetLockDistanceMultiplierSmall = 1 +fTargetLockDistanceMultiplierLarge = 2 +fTargetLockDistanceMultiplierExtraLarge = 4 +fTargetLockPitchAdjustSpeed = 2 +fTargetLockYawAdjustSpeed = 8 +fTargetLockPitchOffsetStrength = 0.25 +fTargetLockMinHeightAboveGround = 35 +uTargetLockArrowAimType = 1 +uTargetLockMissileAimType = 1 +bTargetLockUsePOVSwitchKeyboard = 0 +bTargetLockUsePOVSwitchGamepad = 1 +fTargetLockPOVHoldDuration = 0.25 +bTargetLockUseMouse = 1 +uTargetLockMouseSensitivity = 32 +bTargetLockUseScrollWheel = 1 +bTargetLockUseRightThumbstick = 1 +bResetCameraWithTargetLock = 1 +bResetCameraPitch = 0 +bTargetLockEnableLockBehindTarget = 0 + +[HUD] +bEnableTargetLockReticle = 1 +uReticleAnchor = 0 +uReticleStyle = 0 +fReticleScale = 1 +bReticleUseHUDOpacity = 1 +fReticleOpacity = 1 + +[Misc] +bOverrideAcrobatics = 1 +fAcrobatics = 0.025 +fAcrobaticsGliding = 0.06 + +[Controller] +bOverrideControllerDeadzone = 1 +fControllerRadialDeadzone = 0.24 +fControllerAxialDeadzone = 0.12 +bThumbstickBounceFix = 0 + +[Keys] +uTargetLockKey = 258 +uSwitchTargetLeftKey = -1 +uSwitchTargetRightKey = -1 +uTargetLockBehindTargetKey = -1 \ No newline at end of file diff --git a/scripts/source/TrueDirectionalMovement.psc b/scripts/source/TrueDirectionalMovement.psc index 168481c..24ce4ab 100644 --- a/scripts/source/TrueDirectionalMovement.psc +++ b/scripts/source/TrueDirectionalMovement.psc @@ -4,4 +4,5 @@ bool Function GetDirectionalMovementState() global native bool Function GetTargetLockState() global native Actor Function GetCurrentTarget() global native Function ToggleDisableDirectionalMovement(String asModName, bool abDisable) global native +Function ToggleDisableTargetLock(String asModName, bool abDisable) global native Function ToggleDisableHeadtracking(String asModName, bool abDisable) global native diff --git a/src/API/APIManager.cpp b/src/API/APIManager.cpp index fc063a7..07e6f1b 100644 --- a/src/API/APIManager.cpp +++ b/src/API/APIManager.cpp @@ -41,4 +41,13 @@ void APIs::RequestAPIs() logger::warn("Failed to obtain TrueHUD API"); } } + + if (!IDRC) { + IDRC = reinterpret_cast(IDRC_API::RequestPluginAPI(IDRC_API::InterfaceVersion::V1)); + if (IDRC) { + logger::info("Obtained IDRC API - {0:x}", reinterpret_cast(IDRC)); + } else { + logger::warn("Failed to obtain IDRC API"); + } + } } diff --git a/src/API/APIManager.h b/src/API/APIManager.h index 57d5371..3664225 100644 --- a/src/API/APIManager.h +++ b/src/API/APIManager.h @@ -3,12 +3,14 @@ #include "API/DodgeFrameworkAPI.h" #include "API/SmoothCamAPI.h" #include "API/TrueHUDAPI.h" +#include "API/IDRC_API.h" struct APIs { static inline SmoothCamAPI::IVSmoothCam3* SmoothCam = nullptr; static inline DODGEFRAMEWORK_API::IVDodgeFramework1* DodgeFramework = nullptr; static inline TRUEHUD_API::IVTrueHUD3* TrueHUD = nullptr; + static inline IDRC_API::IVIDRC1* IDRC = nullptr; static void RequestAPIs(); }; diff --git a/src/API/IDRC_API.h b/src/API/IDRC_API.h new file mode 100644 index 0000000..1813cfc --- /dev/null +++ b/src/API/IDRC_API.h @@ -0,0 +1,62 @@ +#pragma once +#include +#include + +/* +* For modders: Copy this file into your own project if you wish to use this API +*/ +namespace IDRC_API { + constexpr const auto IDRCPluginName = "IntuitiveDragonRideControl"; + + // Available IDRC interface versions + enum class InterfaceVersion : uint8_t { + V1 + }; + + // IDRC's modder interface + class IVIDRC1 { + public: + /// + /// Get the thread ID IDRC is running in. + /// You may compare this with the result of GetCurrentThreadId() to help determine + /// if you are using the correct thread. + /// + /// TID + [[nodiscard]] virtual unsigned long GetIDRCThreadId() const noexcept = 0; + + /// + /// Get the actor handle of the the dragon's current target. If case no dragon is currently being ridden, this will return an empty handle. + /// + /// The actor handle of the dragon's current target, or an empty handle if the dragon is not in combat, or no dragon is ridden. + [[nodiscard]] virtual RE::ActorHandle GetCurrentTarget() const noexcept = 0; + + /// + /// Propagates IDRC setting which defines if the dragon's current target should be used by other mods. Used in TrueDirectionalMovement + /// + /// True if current target (obtained via GetCurrentTarget() should be used according to IDRC's settings. + [[nodiscard]] virtual bool UseTarget() const noexcept = 0; + + /// + /// Get the actor handle of the the currently ridden dragon. If case no dragon is currently being ridden, this will return an empty handle. + /// + /// The actor handle of the currently ridden dragon, or an empty handle if no dragon is ridden. + [[nodiscard]] virtual RE::ActorHandle GetDragon() const noexcept = 0; + }; + + typedef void* (*_RequestPluginAPI)(const InterfaceVersion interfaceVersion); + + /// + /// Request the IDRC API interface. + /// Recommended: Send your request during or after SKSEMessagingInterface::kMessage_PostLoad to make sure the dll has already been loaded + /// + /// The interface version to request + /// The pointer to the API singleton, or nullptr if request failed + [[nodiscard]] inline void* RequestPluginAPI(const InterfaceVersion a_interfaceVersion = InterfaceVersion::V1) { + auto pluginHandle = GetModuleHandle("IntuitiveDragonRideControl.dll"); + _RequestPluginAPI requestAPIFunction = (_RequestPluginAPI)GetProcAddress(pluginHandle, "RequestPluginAPI"); + if (requestAPIFunction) { + return requestAPIFunction(a_interfaceVersion); + } + return nullptr; + } +} diff --git a/src/DirectionalMovementHandler.cpp b/src/DirectionalMovementHandler.cpp index 79c709b..c3a2935 100644 --- a/src/DirectionalMovementHandler.cpp +++ b/src/DirectionalMovementHandler.cpp @@ -2,6 +2,7 @@ #include "Settings.h" #include "Events.h" #include "Offsets.h" +#include "DragonCameraState.h" #include #include @@ -247,10 +248,14 @@ void DirectionalMovementHandler::Update() RE::TESObjectREFR* cameraTarget = nullptr; auto thirdPersonState = static_cast(playerCamera->currentState.get()); bool bIsMounted = thirdPersonState->id == RE::CameraState::kMount; + bool bIsMountedDragon = thirdPersonState->id == RE::CameraState::kDragon; if (bIsMounted) { auto horseCameraState = static_cast(thirdPersonState); cameraTarget = horseCameraState->horseRefHandle.get().get(); + } else if (bIsMountedDragon) { + auto dragonCameraState = static_cast(thirdPersonState); + cameraTarget = dragonCameraState->dragonRefHandle.get().get(); } else { cameraTarget = RE::PlayerCharacter::GetSingleton(); } @@ -762,20 +767,29 @@ void DirectionalMovementHandler::UpdateLeaning(RE::Actor* a_actor, [[maybe_unuse void DirectionalMovementHandler::UpdateCameraAutoRotation() { + if (APIs::IDRC && APIs::IDRC->GetDragon()) { + _currentAutoCameraRotationSpeed = 0.f; + return; + } + auto playerCamera = RE::PlayerCamera::GetSingleton(); - if (playerCamera && playerCamera->currentState && (playerCamera->currentState->id == RE::CameraState::kThirdPerson || playerCamera->currentState->id == RE::CameraState::kMount)) { + if (playerCamera && playerCamera->currentState && (playerCamera->currentState->id == RE::CameraState::kThirdPerson + || playerCamera->currentState->id == RE::CameraState::kMount || playerCamera->currentState->id == RE::CameraState::kDragon)) { RE::Actor* cameraTarget = nullptr; auto thirdPersonState = static_cast(playerCamera->currentState.get()); bool bIsMounted = thirdPersonState->id == RE::CameraState::kMount; - + bool bIsMountedDragon = thirdPersonState->id == RE::CameraState::kDragon; if (bIsMounted) { auto horseCameraState = static_cast(thirdPersonState); cameraTarget = horseCameraState->horseRefHandle.get()->As(); + } else if (bIsMountedDragon) { + auto dragonCameraState = static_cast(thirdPersonState); + cameraTarget = dragonCameraState->dragonRefHandle.get()->As(); } else { cameraTarget = RE::PlayerCharacter::GetSingleton(); } - if (!GetFreeCameraEnabled() || (!IsFreeCamera() && !bIsMounted) || _bShouldFaceCrosshair || IsCameraResetting() || HasTargetLocked() || _cameraRotationDelayTimer > 0.f) { + if (!GetFreeCameraEnabled() || (!IsFreeCamera() && !bIsMounted && !bIsMountedDragon) || _bShouldFaceCrosshair || IsCameraResetting() || HasTargetLocked() || _cameraRotationDelayTimer > 0.f) { _currentAutoCameraRotationSpeed = 0.f; return; } @@ -1329,11 +1343,12 @@ bool DirectionalMovementHandler::IsPlayerAIDriven() const if (/*runtimeData.playerFlags.aiControlledToPos || runtimeData.playerFlags.aiControlledFromPos || */runtimeData.playerFlags.aiControlledPackage) { return true; } - - auto& movementController = playerCharacter->GetActorRuntimeData().movementController; - if (movementController && !movementController->playerControls) { - return true; - } +// TODO - Needed to comment this out in order to get it compiled with latest CommonLib version +// (https://github.com/alandtse/CommonLibVR/tree/ng) +// auto& movementController = playerCharacter->GetActorRuntimeData().movementController; +// if (movementController && !movementController->playerControls) { +// return true; +// } return false; } @@ -1360,7 +1375,9 @@ bool DirectionalMovementHandler::IsTDMRotationLocked() const void DirectionalMovementHandler::ResetCamera() { auto playerCamera = RE::PlayerCamera::GetSingleton(); - if (playerCamera->currentState && playerCamera->currentState->id == RE::CameraState::kThirdPerson || playerCamera->currentState->id == RE::CameraState::kMount) { + if (playerCamera->currentState && playerCamera->currentState->id == RE::CameraState::kThirdPerson + || playerCamera->currentState->id == RE::CameraState::kMount + || playerCamera->currentState->id == RE::CameraState::kDragon) { auto playerCharacter = RE::PlayerCharacter::GetSingleton(); auto thirdPersonState = static_cast(playerCamera->currentState.get()); _desiredCameraAngleX = playerCharacter->data.angle.z; @@ -1374,6 +1391,8 @@ void DirectionalMovementHandler::ResetCamera() bool DirectionalMovementHandler::ToggleTargetLock(bool bEnable, bool bPressedManually /*= false */) { + ResetLockBehindTarget(); + auto playerCharacter = RE::PlayerCharacter::GetSingleton(); if (bEnable) { @@ -1382,7 +1401,19 @@ bool DirectionalMovementHandler::ToggleTargetLock(bool bEnable, bool bPressedMan return false; } - RE::ActorHandle actor = FindTarget(bPressedManually ? TargetLockSelectionMode::kCombined : TargetLockSelectionMode::kClosest); + RE::ActorHandle actor; + + if (APIs::IDRC && APIs::IDRC->UseTarget()) + { + // in case the IDRC dragon has a current combat target, lock onto that target + actor = APIs::IDRC->GetCurrentTarget(); + } + + if (!actor) // no target provided by IDRC, use FindTarget() + { + actor = FindTarget(bPressedManually ? TargetLockSelectionMode::kCombined : TargetLockSelectionMode::kClosest); + } + if (actor) { SetTarget(actor); @@ -1421,20 +1452,25 @@ bool DirectionalMovementHandler::ToggleTargetLock(bool bEnable, bool bPressedMan _lastLOSTimer = _lostSightAllowedDuration; auto playerCamera = RE::PlayerCamera::GetSingleton(); - // If on a mount, set player and horse pitch to avoid camera snap - if (playerCharacter->IsOnMount() && playerCamera->currentState && playerCamera->currentState->id == RE::CameraState::kMount) { - auto horseCameraState = static_cast(playerCamera->currentState.get()); - playerCharacter->data.angle.x = -horseCameraState->freeRotation.y; - //horseCameraState->freeRotation.y = 0; - - if (auto horseRefPtr = horseCameraState->horseRefHandle.get()) { - auto horse = horseRefPtr->As(); - if (horse) { - horse->data.angle.x = -horseCameraState->freeRotation.y; + if (playerCharacter->IsOnMount() && playerCamera->currentState) { + if (playerCamera->currentState->id == RE::CameraState::kMount) { + // If on a horse, set player and horse pitch to avoid camera snap + auto horseCameraState = static_cast(playerCamera->currentState.get()); + playerCharacter->data.angle.x = -horseCameraState->freeRotation.y; + //horseCameraState->freeRotation.y = 0; + + if (auto horseRefPtr = horseCameraState->horseRefHandle.get()) { + auto horse = horseRefPtr->As(); + if (horse) { + horse->data.angle.x = -horseCameraState->freeRotation.y; + } } + } else if (playerCamera->currentState->id == RE::CameraState::kDragon) { + // If on a dragon, only set player pitch to avoid camera snap + auto dragonCameraState = static_cast(playerCamera->currentState.get()); + playerCharacter->data.angle.x = -dragonCameraState->freeRotation.y; } } - return true; } @@ -1582,6 +1618,10 @@ void DirectionalMovementHandler::UpdateTargetLock() { ToggleTargetLock(false); } + + if (GetForceDisableTargetLock()) { + ToggleTargetLock(false); + } } } @@ -2203,7 +2243,9 @@ void DirectionalMovementHandler::UpdateCameraHeadtracking() float cameraPitchOffset = 0.f; float cameraYawOffset = 0.f; - if (playerCamera->currentState->id == RE::CameraState::kThirdPerson || playerCamera->currentState->id == RE::CameraState::kMount) + if (playerCamera->currentState->id == RE::CameraState::kThirdPerson + || playerCamera->currentState->id == RE::CameraState::kMount + || playerCamera->currentState->id == RE::CameraState::kDragon) { auto currentState = static_cast(playerCamera->currentState.get()); @@ -2389,7 +2431,9 @@ RE::NiPoint3 DirectionalMovementHandler::GetCameraRotation() ret.x = player->data.angle.x - angle.x; ret.y = angle.y; ret.z = player->data.angle.z; //NormalAbsoluteAngle(-angle.z); - } else if (playerCamera->currentState->id == RE::CameraStates::kThirdPerson || playerCamera->currentState->id == RE::CameraStates::kMount) { + } else if (playerCamera->currentState->id == RE::CameraStates::kThirdPerson + || playerCamera->currentState->id == RE::CameraStates::kMount + || playerCamera->currentState->id == RE::CameraStates::kDragon) { const auto thirdPersonState = static_cast(playerCamera->currentState.get()); ret.x = player->data.angle.x + thirdPersonState->freeRotation.y; ret.y = 0.f; @@ -2401,6 +2445,148 @@ RE::NiPoint3 DirectionalMovementHandler::GetCameraRotation() return ret; } +void DirectionalMovementHandler::ResetLockBehindTarget() +{ + _moveCameraBehindTarget = false; + _isBehind = false; + _moveCameraBehindTarget_prev = false; + _isBehind_prev = false; + _enableLockBehindTarget = false; + _isLockedCameraTransitioning = false; + _isLockedCameraTransitioning_prev = false; +} + +void DirectionalMovementHandler::ToggleLockBehindTarget() +{ + if (HasTargetLocked() && Settings::bTargetLockEnableLockBehindTarget) + { + if(_isLockedCameraTransitioning) { + return; + } + + _enableLockBehindTarget = !_enableLockBehindTarget; + _isLockedCameraTransitioning = true; + } +} + +float DirectionalMovementHandler::GetNominalCameraToPlayerDistance() const +{ + // GetNominalCameraToPlayerDistance() will provide the nominal distance, not the actual distance + // The actual distance can be smaller in case the camera collides with the environment + + RE::ThirdPersonState* thirdPersonState = nullptr; + + auto playerCamera = RE::PlayerCamera::GetSingleton(); + bool bIsHorseCamera = playerCamera->currentState->id == RE::CameraState::kMount; + bool bIsDragonCamera = playerCamera->currentState->id == RE::CameraState::kDragon; + + if (playerCamera && playerCamera->currentState && (playerCamera->currentState->id == RE::CameraState::kThirdPerson || bIsHorseCamera || bIsDragonCamera)) { + thirdPersonState = static_cast(playerCamera->currentState.get()); + } + + if (!thirdPersonState) { + logger::warn("GetNominalCameraToPlayerDistance - No valid third person camera state found"); + return 0.f; + } + + float cameraToPlayerDist = thirdPersonState->posOffsetActual.Length(); + if (bIsHorseCamera) { + auto horseCameraState = static_cast(thirdPersonState); + cameraToPlayerDist = horseCameraState->posOffsetActual.Length(); + } else if (bIsDragonCamera) { + auto dragonCameraState = static_cast(thirdPersonState); + // in-game player-camera distance for the dragon camera is not posOffsetActual.Length(), + // but scaled by the target zoom offset as below: + cameraToPlayerDist = (3.0f + 2.0f * thirdPersonState->targetZoomOffset) * dragonCameraState->posOffsetActual.Length(); + } + return cameraToPlayerDist; +} + +RE::NiPoint3 DirectionalMovementHandler::GetNominalCameraPosition(const RE::NiPoint3& a_playerPos, const RE::NiPoint3& a_cameraPos) const +{ + RE::NiPoint3 cameraDirectionToPlayer = RE::NiPoint3(a_playerPos.x - a_cameraPos.x, a_playerPos.y - a_cameraPos.y, a_playerPos.z - a_cameraPos.z); + cameraDirectionToPlayer.Unitize(); + float nominalCameraToPlayerDist = GetNominalCameraToPlayerDistance(); + // vector pointing from the nominal camera position to the player + RE::NiPoint3 nominalCameraToPlayer = cameraDirectionToPlayer * nominalCameraToPlayerDist; + + // nominal position for the camera, ignoring potential camera collision with environment (collision changes camera-player distance) + RE::NiPoint3 nominalCameraPos = a_playerPos - nominalCameraToPlayer; + if (nominalCameraToPlayerDist < 1.0f) { + // nominalCameraToPlayerDist is 0 when player is aiming with a bow - use actual distance as fallback + nominalCameraPos = a_cameraPos; + } + return nominalCameraPos; +} + +void DirectionalMovementHandler::UpdateMoveCameraBehindTarget(const float a_distanceToTarget) +{ + RE::NiPoint3 playerPos; + if (!GetTorsoPos(RE::PlayerCharacter::GetSingleton(), playerPos)) { + return; + } + + if(_isLockedCameraTransitioning) { + return; + } + + float cameraToPlayerDist = playerPos.GetDistance(GetCameraPos()); + float nominalCameraToPlayerDist = GetNominalCameraToPlayerDistance(); + if (nominalCameraToPlayerDist < 1.0f) { + // nominalCameraToPlayerDist is 0 when player is aiming with a bow - use actual distance as fallback + nominalCameraToPlayerDist = cameraToPlayerDist; + } + + if (_enableLockBehindTarget) + { + float fScale = 1.f; + + // Adjust fScale to compensate for the larger camera-player distance in the dragon camera state + auto playerCamera = RE::PlayerCamera::GetSingleton(); + if (playerCamera && playerCamera->currentState && playerCamera->currentState->id == RE::CameraState::kDragon ) + { + RE::DragonCameraState* dragonCameraState = nullptr; + dragonCameraState = static_cast(playerCamera->currentState.get()); + if (dragonCameraState) + { + if (auto dragonRefPtr = dragonCameraState->dragonRefHandle.get()) + { + auto* dragonActor = dragonRefPtr->As(); + if (dragonActor && GetFlyingState(dragonActor) != 0) + { + // disable lock behind target in case the dragon mount is not grounded + if (_moveCameraBehindTarget) { + _isLockedCameraTransitioning = true; + } + _moveCameraBehindTarget = false; + return; + } + } + + fScale = 0.5f * (3.0f + 2.0f * dragonCameraState->targetZoomOffset); + } + } + + if (!_moveCameraBehindTarget && nominalCameraToPlayerDist > a_distanceToTarget + fScale * Settings::fCameraBehindTargetMinDistance + fScale * Settings::fCameraBehindTargetNoSwitchRange) + { + // switch to behind-target position if player-camera distance is more than fCameraBehindTargetMinDistance + fCameraBehindTargetNoSwitchRange larger than the player-target distance + _moveCameraBehindTarget = true; + _isLockedCameraTransitioning = true; + } else if (_moveCameraBehindTarget && nominalCameraToPlayerDist < a_distanceToTarget + fScale * Settings::fCameraBehindTargetMinDistance) + { + // switch to normal position if player-camera distance is less than fCameraBehindTargetMinDistance behind the target + _moveCameraBehindTarget = false; + _isLockedCameraTransitioning = true; + } + } else + { + if (_moveCameraBehindTarget) { + _isLockedCameraTransitioning = true; + } + _moveCameraBehindTarget = false; + } +} + // probably bad math ahead void DirectionalMovementHandler::LookAtTarget(RE::ActorHandle a_target) { @@ -2419,8 +2605,9 @@ void DirectionalMovementHandler::LookAtTarget(RE::ActorHandle a_target) RE::ThirdPersonState* thirdPersonState = nullptr; bool bIsHorseCamera = playerCamera->currentState->id == RE::CameraState::kMount; + bool bIsDragonCamera = playerCamera->currentState->id == RE::CameraState::kDragon; - if (playerCamera && playerCamera->currentState && (playerCamera->currentState->id == RE::CameraState::kThirdPerson || bIsHorseCamera)) { + if (playerCamera && playerCamera->currentState && (playerCamera->currentState->id == RE::CameraState::kThirdPerson || bIsHorseCamera || bIsDragonCamera)) { thirdPersonState = static_cast(playerCamera->currentState.get()); } @@ -2433,7 +2620,23 @@ void DirectionalMovementHandler::LookAtTarget(RE::ActorHandle a_target) return; } - float currentCharacterYaw = playerCharacter->data.angle.z; + // In case player is mounted on a dragon, need to use dragon as reference for the currentCharacterYaw: + // Reason: the player sits on the dragon's neck, which keeps moving as the dragon changes the look direction. + // So the player's yaw changes along with the dragon's look direction, derailing the camera target position. + // Using the dragon's yaw instead solves this. + RE::Actor* yawActor = static_cast(playerCharacter); + if (bIsDragonCamera) { + auto dragonCameraState = static_cast(thirdPersonState); + if (auto dragonRefPtr = dragonCameraState->dragonRefHandle.get()) + { + yawActor = dragonRefPtr->As(); + } else + { + logger::warn("LookAtTarget - Failed to get dragon for yaw"); + } + } + + float currentCharacterYaw = yawActor->data.angle.z; float currentCharacterPitch = playerCharacter->data.angle.x; float currentCameraYawOffset = NormalAbsoluteAngle(thirdPersonState->freeRotation.x); @@ -2442,14 +2645,25 @@ void DirectionalMovementHandler::LookAtTarget(RE::ActorHandle a_target) //RE::NiPoint3 midPoint = (playerPos + targetPos) / 2; float distanceToTarget = playerPos.GetDistance(targetPos); + + UpdateMoveCameraBehindTarget(distanceToTarget); + float zOffset = distanceToTarget * Settings::fTargetLockPitchOffsetStrength; - if (bIsHorseCamera) { - zOffset *= -1.f; + // scaling compensates for the larger camera-player distance in the dragon camera state + float fScale = bIsDragonCamera ? 0.5f * (3.0f + 2.0f * thirdPersonState->targetZoomOffset) : 1.f; + if (_moveCameraBehindTarget) + { + float nominalCameraToPlayerDist = GetNominalCameraPosition(playerPos, cameraPos).GetDistance(playerPos); + // multiplier reduces offset as camera gets closer to target. + float offsetMultiplier = 1.f - (distanceToTarget + fScale * Settings::fCameraBehindTargetMinDistance) / nominalCameraToPlayerDist; + offsetMultiplier = Clamp(offsetMultiplier, 0.f, 1.f); + zOffset *= offsetMultiplier; } + zOffset /= fScale; RE::NiPoint3 offsetTargetPos = targetPos; - offsetTargetPos.z -= zOffset; + offsetTargetPos.z = _moveCameraBehindTarget ? offsetTargetPos.z + zOffset : offsetTargetPos.z - zOffset; //offsetTargetPos = midPoint; RE::NiPoint3 playerToTarget = RE::NiPoint3(targetPos.x - playerPos.x, targetPos.y - playerPos.y, targetPos.z - playerPos.z); @@ -2460,43 +2674,109 @@ void DirectionalMovementHandler::LookAtTarget(RE::ActorHandle a_target) cameraDirectionToTarget.Unitize(); RE::NiPoint3 cameraToPlayer = RE::NiPoint3(playerPos.x - cameraPos.x, playerPos.y - cameraPos.y, playerPos.z - cameraPos.z); - RE::NiPoint3 projected = Project(cameraToPlayer, cameraToTarget); - RE::NiPoint3 projectedPos = RE::NiPoint3(projected.x + cameraPos.x, projected.y + cameraPos.y, projected.z + cameraPos.z); - RE::NiPoint3 projectedDirectionToTarget = RE::NiPoint3(targetPos.x - projectedPos.x, targetPos.y - projectedPos.y, targetPos.z - projectedPos.z); - projectedDirectionToTarget.Unitize(); + // If the camera should move behind the target, use the camera-player line as 1st reference for deltaAngle + // If the camera should move behind the player, use the camera-target line as 1st reference for deltaAngle + // This ensures that for both cases deltaAngle gets smaller as the camera approaches the target line + RE::NiPoint3 from = _moveCameraBehindTarget ? playerToTarget : cameraToPlayer; + RE::NiPoint3 onto = _moveCameraBehindTarget ? cameraToPlayer : cameraToTarget; + RE::NiPoint3 projected = Project(from, onto); + RE::NiPoint3 projectedPos = _moveCameraBehindTarget ? projected + playerPos : projected + cameraPos; + RE::NiPoint3 referencePos = _moveCameraBehindTarget ? playerPos : targetPos; + RE::NiPoint3 projectedDirection = referencePos - projectedPos; + projectedDirection.Unitize(); + RE::NiPoint2 projectedDirectionXY(-projectedDirection.x, projectedDirection.y); // yaw + RE::NiPoint2 forwardVector(0.f, 1.f); - RE::NiPoint2 currentCameraDirection = Vec2Rotate(forwardVector, currentCharacterYaw + currentCameraYawOffset); + // If the camera should move behind the player, _isBehind is computed relative to the camera-target line + RE::NiPoint2 isBehindDirection = Vec2Rotate(forwardVector, currentCharacterYaw + currentCameraYawOffset); + if (_moveCameraBehindTarget) { + // If the camera should move behind the target, _isBehind is computed relative to the camera-player line + isBehindDirection.x = -cameraToPlayer.x; + isBehindDirection.y = cameraToPlayer.y; + isBehindDirection.Unitize(); + } - RE::NiPoint2 projectedDirectionToTargetXY(-projectedDirectionToTarget.x, projectedDirectionToTarget.y); + _isBehind = (projectedDirectionXY.Dot(isBehindDirection) < 0); - bool bIsBehind = projectedDirectionToTargetXY.Dot(currentCameraDirection) < 0; + if (_isLockedCameraTransitioning && !_isLockedCameraTransitioning_prev) + { + _isBehind_prev = _isBehind; + _moveCameraBehindTarget_prev = _moveCameraBehindTarget; + } + _isLockedCameraTransitioning_prev = _isLockedCameraTransitioning; + if (Settings::bTargetLockEnableLockBehindTarget && _isLockedCameraTransitioning && !_isBehind_prev && _moveCameraBehindTarget == _moveCameraBehindTarget_prev) + { + // keep camera movement stable during camera collisions with the environment + _isBehind = false; + } + + _moveCameraBehindTarget_prev = _moveCameraBehindTarget; + _isBehind_prev = _isBehind; + + // If the camera should move behind the player, use the camera-target line as 2nd reference for deltaAngle + RE::NiPoint2 currentCameraDirection = isBehindDirection; + if (_moveCameraBehindTarget) { + // If the camera should move behind the target, use the target-player line as 2nd reference for deltaAngle + // Using the camera-target line would result in large angle variations (ie camera oscillations) when the camera is close to the target + currentCameraDirection.x = playerToTarget.x; + currentCameraDirection.y = -playerToTarget.y; + currentCameraDirection.Unitize(); + } auto reversedCameraDirection = currentCameraDirection * -1.f; - float angleDelta = bIsBehind ? GetAngle(reversedCameraDirection, projectedDirectionToTargetXY) : GetAngle(currentCameraDirection, projectedDirectionToTargetXY); + float behindAngle = _isLockedCameraTransitioning ? -PI2 : GetAngle(reversedCameraDirection, projectedDirectionXY); // always transition counterclockwise + float angleDelta = _isBehind ? behindAngle : GetAngle(currentCameraDirection, projectedDirectionXY); + if (_moveCameraBehindTarget) { + float cameraDirectionYaw = std::atan2(currentCameraDirection.x, currentCameraDirection.y); + RE::NiQuaternion cameraRotation; + thirdPersonState->GetRotation(cameraRotation); + angleDelta = GetYaw(cameraRotation) - cameraDirectionYaw; + } angleDelta = NormalRelativeAngle(angleDelta); - const float realTimeDeltaTime = GetRealTimeDeltaTime(); + // Clamp realTimeDeltaTime to max 50ms/frame. Prevents too fast camera rotation when the game is running at low FPS. + const float realTimeDeltaTime = GetRealTimeDeltaTime() < 0.05f ? GetRealTimeDeltaTime() : 0.05f; float desiredFreeCameraRotation = currentCameraYawOffset + angleDelta; + if (_isLockedCameraTransitioning && !_isBehind && fabs(angleDelta) < PI/180.f) { + _isLockedCameraTransitioning = false; + _isLockedCameraTransitioning_prev = false; + } + thirdPersonState->freeRotation.x = InterpAngleTo(currentCameraYawOffset, desiredFreeCameraRotation, realTimeDeltaTime, Settings::fTargetLockYawAdjustSpeed); - if (bIsBehind) + if (_isBehind) { return; // don't adjust pitch } // pitch RE::NiPoint3 playerAngle = ToOrientationRotation(playerDirectionToTarget); - RE::NiPoint3 cameraAngle = ToOrientationRotation(cameraDirectionToTarget); + + // If the camera should move behind the player, use the camera-offsetTarget line as reference for pitch + RE::NiPoint3 offsetCameraDirection = cameraDirectionToTarget; + if (_moveCameraBehindTarget) { + // If the camera should move behind the target, use the player-offsetTarget line as reference for pitch + offsetCameraDirection = playerPos - offsetTargetPos; + offsetCameraDirection.Unitize(); + } + RE::NiPoint3 cameraAngle = GetCameraAngle(playerPos, cameraPos, offsetCameraDirection); _desiredPlayerPitch = -playerAngle.x; - cameraAngle.x *= ((PI - fabs(cameraAngle.x)) / PI); - float desiredCameraAngle = _desiredPlayerPitch + cameraAngle.x; + + float referencePitch = _desiredPlayerPitch; + if (bIsHorseCamera || bIsDragonCamera) { + // for horse and dragon cameras, the reference pitch is always 0 + referencePitch = 0.f; + } + float desiredCameraAngle = referencePitch + cameraAngle.x; playerCharacter->data.angle.x = _desiredPlayerPitch; // player pitch if (bIsHorseCamera) { + // update pitch only when riding a horse, not a dragon + // changing the dragon's pitch would result in flickering of the dragon's orientation while it transitions between flying states auto horseCameraState = static_cast(thirdPersonState); if (auto horseRefPtr = horseCameraState->horseRefHandle.get()) { auto horse = horseRefPtr->As(); @@ -2508,12 +2788,37 @@ void DirectionalMovementHandler::LookAtTarget(RE::ActorHandle a_target) float cameraPitchOffset = _desiredPlayerPitch - currentCharacterPitch; - if (!bIsHorseCamera) { + if (!bIsHorseCamera && !bIsDragonCamera) { thirdPersonState->freeRotation.y += cameraPitchOffset; - thirdPersonState->freeRotation.y = InterpAngleTo(thirdPersonState->freeRotation.y, desiredCameraAngle, realTimeDeltaTime, Settings::fTargetLockPitchAdjustSpeed); - } else { - thirdPersonState->freeRotation.y = InterpAngleTo(thirdPersonState->freeRotation.y, -desiredCameraAngle, realTimeDeltaTime, Settings::fTargetLockPitchAdjustSpeed); } + thirdPersonState->freeRotation.y = InterpAngleTo(thirdPersonState->freeRotation.y, desiredCameraAngle, realTimeDeltaTime, Settings::fTargetLockPitchAdjustSpeed); +} + +RE::NiPoint3 DirectionalMovementHandler::GetCameraAngle(RE::NiPoint3& a_playerPos, RE::NiPoint3& a_cameraPos, RE::NiPoint3& a_cameraDirection) +{ + RE::NiPoint3 cameraAngle = ToOrientationRotation(a_cameraDirection); + + if (!RE::PlayerCharacter::GetSingleton()->GetParentCell()->IsInteriorCell()) { + // determine vertical projection of the camera position to the minimal height above ground + RE::NiPoint3 offsetGroundPos = a_cameraPos; + offsetGroundPos.z = GetLandHeightWithWater(a_cameraPos) + Settings::fTargetLockMinHeightAboveGround; + RE::NiPoint3 offsetGroundPosToPlayer = RE::NiPoint3(a_playerPos.x - offsetGroundPos.x, a_playerPos.y - offsetGroundPos.y, a_playerPos.z - offsetGroundPos.z); + RE::NiPoint3 offsetGroundPosToPlayerDirection = offsetGroundPosToPlayer; + offsetGroundPosToPlayerDirection.Unitize(); + + // get the angle from the offsetGroundPos to the player + RE::NiPoint3 offsetGroundPosAngle = ToOrientationRotation(offsetGroundPosToPlayerDirection); + + // use offsetGroundPosAngle to adjust the camera pitch if needed + if (offsetGroundPosAngle.x < cameraAngle.x) { + // don't go below the angle of the final camera <-> target direction + cameraAngle.x = offsetGroundPosAngle.x; + } + } + + cameraAngle.x *= ((PI - fabs(cameraAngle.x)) / PI); + + return cameraAngle; } void DirectionalMovementHandler::UpdateAIProcessRotationSpeed(RE::Actor* a_actor) @@ -2557,6 +2862,7 @@ void DirectionalMovementHandler::OnPreLoadGame() _dialogueSpeaker = RE::ObjectRefHandle(); _playerIsNPC = false; _papyrusDisableDirectionalMovement.clear(); + _papyrusDisableTargetLock.clear(); _papyrusDisableHeadtracking.clear(); } @@ -2712,6 +3018,15 @@ void DirectionalMovementHandler::PapyrusDisableDirectionalMovement(std::string_v } } +void DirectionalMovementHandler::PapyrusDisableTargetLock(std::string_view a_modName, bool a_bDisable) +{ + if (a_bDisable) { + _papyrusDisableTargetLock.emplace(a_modName.data()); + } else { + _papyrusDisableTargetLock.erase(a_modName.data()); + } +} + void DirectionalMovementHandler::PapyrusDisableHeadtracking(std::string_view a_modName, bool a_bDisable) { if (a_bDisable) { diff --git a/src/DirectionalMovementHandler.h b/src/DirectionalMovementHandler.h index 254954f..5612462 100644 --- a/src/DirectionalMovementHandler.h +++ b/src/DirectionalMovementHandler.h @@ -191,13 +191,21 @@ class DirectionalMovementHandler : RE::NiPoint3 GetCameraRotation(); + void ResetLockBehindTarget(); + void ToggleLockBehindTarget(); + float GetNominalCameraToPlayerDistance() const; + RE::NiPoint3 GetNominalCameraPosition(const RE::NiPoint3& a_playerPos, const RE::NiPoint3& a_cameraPos) const; + void UpdateMoveCameraBehindTarget(const float a_distanceToTarget); void LookAtTarget(RE::ActorHandle a_target); + RE::NiPoint3 GetCameraAngle(RE::NiPoint3& a_playerPos, RE::NiPoint3& a_cameraPos, RE::NiPoint3& a_cameraDirection); bool ShouldFaceTarget() const { return _bShouldFaceTarget; } bool ShouldFaceCrosshair() const { return _bShouldFaceCrosshair; } bool HasTargetLocked() const { return static_cast(_target); } + bool IsTargetLockBehindTarget() const { return HasTargetLocked() && _moveCameraBehindTarget && !_isBehind; } + float GetDialogueHeadtrackTimer() const { return _dialogueHeadtrackTimer; } void RefreshDialogueHeadtrackTimer() { _dialogueHeadtrackTimer = Settings::fDialogueHeadtrackingDuration; } float GetCameraHeadtrackTimer() const { return _cameraHeadtrackTimer; } @@ -221,14 +229,17 @@ class DirectionalMovementHandler : std::atomic_bool _bReticleRemoved{ false }; bool GetForceDisableDirectionalMovement() const { return _bForceDisableDirectionalMovement || !_papyrusDisableDirectionalMovement.empty(); } + bool GetForceDisableTargetLock() const { return _bForceDisableTargetLock || !_papyrusDisableTargetLock.empty(); } bool GetForceDisableHeadtracking() const { return _bForceDisableHeadtracking || !_papyrusDisableHeadtracking.empty(); } bool GetYawControl() const { return _bYawControlledByPlugin; } void SetForceDisableDirectionalMovement(bool a_disable) { _bForceDisableDirectionalMovement = a_disable; } + void SetForceDisableTargetLock(bool a_disable) { _bForceDisableTargetLock = a_disable; } void SetForceDisableHeadtracking(bool a_disable) { _bForceDisableHeadtracking = a_disable; } void SetYawControl(bool a_enable, float a_yawRotationSpeedMultiplier = 0); void SetPlayerYaw(float a_yaw) { _desiredAngle = NormalAbsoluteAngle(a_yaw); } void PapyrusDisableDirectionalMovement(std::string_view a_modName, bool a_bDisable); + void PapyrusDisableTargetLock(std::string_view a_modName, bool a_bDisable); void PapyrusDisableHeadtracking(std::string_view a_modName, bool a_bDisable); bool IsACCInstalled() const { return _bACCInstalled; } @@ -267,6 +278,14 @@ class DirectionalMovementHandler : bool _bUpdatePlayerPitch = false; float _desiredPlayerPitch; + bool _enableLockBehindTarget = false; + bool _moveCameraBehindTarget = false; // tracks if targeted camera position is behind the target + bool _moveCameraBehindTarget_prev = false; // tracks state from previous frame + bool _isBehind = false; + bool _isBehind_prev = false; // tracks state from previous frame + bool _isLockedCameraTransitioning = false; + bool _isLockedCameraTransitioning_prev = false; + bool _bResetCamera = false; float _desiredCameraAngleX; float _desiredCameraAngleY; @@ -330,6 +349,8 @@ class DirectionalMovementHandler : bool _bForceDisableDirectionalMovement = false; std::unordered_set _papyrusDisableDirectionalMovement{}; + bool _bForceDisableTargetLock = false; + std::unordered_set _papyrusDisableTargetLock{}; bool _bForceDisableHeadtracking = false; std::unordered_set _papyrusDisableHeadtracking{}; bool _bYawControlledByPlugin = false; diff --git a/src/DragonCameraState.h b/src/DragonCameraState.h new file mode 100644 index 0000000..b1e038c --- /dev/null +++ b/src/DragonCameraState.h @@ -0,0 +1,42 @@ +#pragma once + +#include "RE/T/ThirdPersonState.h" + +namespace RE +{ + class NiNode; + + class DragonCameraState : public ThirdPersonState + { + public: + inline static constexpr auto RTTI = RTTI_DragonCameraState; + inline static constexpr auto VTABLE = VTABLE_DragonCameraState; + + ~DragonCameraState() override; // 00 + + // override (ThirdPersonState) + void Begin() override; // 01 + void End() override; // 02 + void SaveGame(BGSSaveFormBuffer* a_buf) override; // 06 + void LoadGame(BGSLoadFormBuffer* a_buf) override; // 07 + void Revert(BGSLoadFormBuffer* a_buf) override; // 08 + void SetCameraHandle(RefHandle& a_handle) override; // 09 - { return; } + void Unk_0A(void) override; // 0A - { return; } + void ProcessWeaponDrawnChange(bool a_drawn) override; // 0B + bool GetFreeRotationMode() const override; // 0C + void SetFreeRotationMode(bool a_weaponSheathed) override; // 0D + void HandleLookInput(const NiPoint2& a_input) override; // 0F + + // members + ObjectRefHandle dragonRefHandle; // E8 + float dragonCurrentDirection; // EC + std::uint64_t unkF0; // F0 + private: + KEEP_FOR_RE() + }; +#if defined(EXCLUSIVE_SKYRIM_VR) + static_assert(sizeof(DragonCameraState) == 0x110); +#else + static_assert(sizeof(DragonCameraState) == 0xF8); +#endif +} \ No newline at end of file diff --git a/src/Events.cpp b/src/Events.cpp index 3f619ca..e81cd19 100644 --- a/src/Events.cpp +++ b/src/Events.cpp @@ -60,6 +60,11 @@ namespace Events continue; } + if (key == Settings::uTargetLockBehindTargetKey) + { + DirectionalMovementHandler::GetSingleton()->ToggleLockBehindTarget(); + } + if (key == Settings::uTargetLockKey) { bool bIgnore = false; diff --git a/src/Hooks.cpp b/src/Hooks.cpp index b2ab21b..a135a9a 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -14,7 +14,7 @@ namespace Hooks kNone, kFirstPerson, kThirdPerson, - kHorse + kMount }; RotationType rotationType = RotationType::kNone; @@ -102,6 +102,7 @@ namespace Hooks FirstPersonStateHook::Hook(); ThirdPersonStateHook::Hook(); HorseCameraStateHook::Hook(); + DragonCameraStateHook::Hook(); TweenMenuCameraStateHook::Hook(); VATSCameraStateHook::Hook(); PlayerCameraTransitionStateHook::Hook(); @@ -516,7 +517,7 @@ namespace Hooks _OnEnterState(a_this); if (DirectionalMovementHandler::GetSingleton()->GetFreeCameraEnabled()) { - if (savedCamera.rotationType == SaveCamera::RotationType::kHorse) { + if (savedCamera.rotationType == SaveCamera::RotationType::kMount) { a_this->freeRotation.x = savedCamera.ConsumeYaw(); } @@ -674,7 +675,7 @@ namespace Hooks void HorseCameraStateHook::OnExitState(RE::HorseCameraState* a_this) { if (DirectionalMovementHandler::GetSingleton()->GetFreeCameraEnabled()) { - savedCamera.SaveRotation(a_this->freeRotation, SaveCamera::RotationType::kHorse); + savedCamera.SaveRotation(a_this->freeRotation, SaveCamera::RotationType::kMount); savedCamera.SaveZoom(a_this->currentZoomOffset, a_this->pitchZoomOffset); } @@ -749,7 +750,125 @@ namespace Hooks _ProcessButton(a_this, a_event, a_data); } + +// DragonCameraStateHook implementation is identical to HorseCameraStateHook + void DragonCameraStateHook::OnEnterState(RE::DragonCameraState* a_this) + { + _OnEnterState(a_this); + + if (DirectionalMovementHandler::GetSingleton()->GetFreeCameraEnabled()) { + auto playerCharacter = RE::PlayerCharacter::GetSingleton(); + + RE::Actor* dragon = nullptr; + dragon = static_cast(a_this->dragonRefHandle.get().get()); + + if (savedCamera.rotationType != SaveCamera::RotationType::kNone) { + auto rotationType = savedCamera.rotationType; + RE::NiPoint2 rot = savedCamera.ConsumeRotation(); + if (rotationType == SaveCamera::RotationType::kThirdPerson) { + playerCharacter->data.angle.x = -rot.y; + } + + a_this->freeRotation.x = NormalAbsoluteAngle(rot.x - dragon->data.angle.z); + } + + if (savedCamera.bZoomSaved) { + float zoomOffset, pitchZoomOffset; + savedCamera.ConsumeZoom(zoomOffset, pitchZoomOffset); + a_this->targetZoomOffset = zoomOffset; + a_this->currentZoomOffset = a_this->targetZoomOffset; + a_this->savedZoomOffset = a_this->targetZoomOffset; + } + + a_this->dragonCurrentDirection = dragon->GetHeading(false); + } + } + + void DragonCameraStateHook::OnExitState(RE::DragonCameraState* a_this) + { + if (DirectionalMovementHandler::GetSingleton()->GetFreeCameraEnabled()) { + savedCamera.SaveRotation(a_this->freeRotation, SaveCamera::RotationType::kMount); + savedCamera.SaveZoom(a_this->currentZoomOffset, a_this->pitchZoomOffset); + } + + _OnExitState(a_this); + } + + void DragonCameraStateHook::UpdateRotation(RE::DragonCameraState* a_this) + { + if (APIs::IDRC && APIs::IDRC->GetDragon()) { + _UpdateRotation(a_this); + return; + } + + auto directionalMovementHandler = DirectionalMovementHandler::GetSingleton(); + if (directionalMovementHandler->GetFreeCameraEnabled() && !directionalMovementHandler->IFPV_IsFirstPerson() && !directionalMovementHandler->ImprovedCamera_IsFirstPerson()) { + float dragonCurrentDirection = a_this->dragonCurrentDirection; + float freeRotationX = a_this->freeRotation.x; + + a_this->freeRotationEnabled = true; + + _UpdateRotation(a_this); + + a_this->dragonCurrentDirection = dragonCurrentDirection; + a_this->freeRotation.x = freeRotationX; + + if (a_this->dragonRefHandle) { + RE::Actor* dragon = nullptr; + dragon = static_cast(a_this->dragonRefHandle.get().get()); + if (dragon) { + float heading = dragon->GetHeading(false); + + a_this->freeRotation.x += a_this->dragonCurrentDirection - heading; + + NiQuaternion_SomeRotationManipulation(a_this->rotation, -a_this->freeRotation.y, 0.f, heading + a_this->freeRotation.x); + a_this->dragonCurrentDirection = heading; + } + } + } else { + _UpdateRotation(a_this); + } + } + + void DragonCameraStateHook::HandleLookInput(RE::DragonCameraState* a_this, const RE::NiPoint2& a_input) + { + if (DirectionalMovementHandler::GetSingleton()->HasTargetLocked()) { + return; + } + + _HandleLookInput(a_this, a_input); + } + + void DragonCameraStateHook::ProcessButton(RE::DragonCameraState* a_this, RE::ButtonEvent* a_event, RE::PlayerControlsData* a_data) + { + auto directionalMovementHandler = DirectionalMovementHandler::GetSingleton(); + if (a_event && directionalMovementHandler->HasTargetLocked() && Settings::bTargetLockUseScrollWheel) { + auto& userEvent = a_event->QUserEvent(); + auto userEvents = RE::UserEvents::GetSingleton(); + + if (userEvent == userEvents->zoomIn) { + directionalMovementHandler->SwitchTarget(DirectionalMovementHandler::Direction::kLeft); + return; + } else if (userEvent == userEvents->zoomOut) { + directionalMovementHandler->SwitchTarget(DirectionalMovementHandler::Direction::kRight); + return; + } + } + + if (a_event && BSInputDeviceManager_IsUsingGamepad(RE::BSInputDeviceManager::GetSingleton()) ? Settings::bTargetLockUsePOVSwitchGamepad : Settings::bTargetLockUsePOVSwitchKeyboard) { + auto& userEvent = a_event->QUserEvent(); + auto userEvents = RE::UserEvents::GetSingleton(); + + if (userEvent == userEvents->togglePOV && a_event->IsUp() && a_event->HeldDuration() < Settings::fTargetLockPOVHoldDuration) { + //directionalMovementHandler->ToggleTargetLock(!directionalMovementHandler->HasTargetLocked()); + return; + } + } + + _ProcessButton(a_this, a_event, a_data); + } + void TweenMenuCameraStateHook::OnEnterState(RE::TESCameraState* a_this) { if (DirectionalMovementHandler::GetSingleton()->IsFreeCamera()) { @@ -779,15 +898,17 @@ namespace Hooks void PlayerCameraTransitionStateHook::OnEnterState(RE::PlayerCameraTransitionState* a_this) { - if (a_this->transitionFrom->id == RE::CameraStates::kMount && a_this->transitionTo->id == RE::CameraStates::kThirdPerson) { - if (savedCamera.rotationType == SaveCamera::RotationType::kHorse) { + if ((a_this->transitionFrom->id == RE::CameraStates::kMount || a_this->transitionFrom->id == RE::CameraStates::kDragon) + && a_this->transitionTo->id == RE::CameraStates::kThirdPerson) { + if (savedCamera.rotationType == SaveCamera::RotationType::kMount) { auto thirdPersonState = static_cast(a_this->transitionTo); auto playerCharacter = RE::PlayerCharacter::GetSingleton(); thirdPersonState->freeRotation.x = savedCamera.ConsumeYaw(); playerCharacter->data.angle.x = -savedCamera.ConsumePitch(); } - } else if (a_this->transitionFrom->id == RE::CameraStates::kMount && a_this->transitionTo->id == RE::CameraStates::kFirstPerson) { - if (savedCamera.rotationType == SaveCamera::RotationType::kHorse) { + } else if ((a_this->transitionFrom->id == RE::CameraStates::kMount || a_this->transitionFrom->id == RE::CameraStates::kDragon) + && a_this->transitionTo->id == RE::CameraStates::kFirstPerson) { + if (savedCamera.rotationType == SaveCamera::RotationType::kMount) { auto playerCharacter = RE::PlayerCharacter::GetSingleton(); playerCharacter->data.angle.x = -savedCamera.ConsumePitch(); } @@ -1002,12 +1123,22 @@ namespace Hooks auto directionalMovementHandler = DirectionalMovementHandler::GetSingleton(); if (!directionalMovementHandler->HasTargetLocked() && directionalMovementHandler->GetCurrentlyMountedAiming()) { auto playerCamera = RE::PlayerCamera::GetSingleton(); - if (playerCamera->currentState && playerCamera->currentState->id == RE::CameraState::kMount) { - auto horseCameraState = static_cast(playerCamera->currentState.get()); + if (playerCamera->currentState + && (playerCamera->currentState->id == RE::CameraState::kMount || playerCamera->currentState->id == RE::CameraState::kDragon)) { constexpr RE::NiPoint3 forwardVector{ 0.f, 1.f, 0.f }; constexpr RE::NiPoint3 upVector{ 0.f, 0.f, 1.f }; RE::NiQuaternion cameraRotation; - horseCameraState->GetRotation(cameraRotation); + if (playerCamera->currentState->id == RE::CameraState::kMount) { + auto horseCameraState = static_cast(playerCamera->currentState.get()); + horseCameraState->GetRotation(cameraRotation); + } else if (playerCamera->currentState->id == RE::CameraState::kDragon) { + auto dragonCameraState = static_cast(playerCamera->currentState.get()); + dragonCameraState->GetRotation(cameraRotation); + } else { + logger::error("ProjectileHook::InitProjectile: PlayerCamera current state is not a horse or dragon camera state, cannot set projectile rotation."); + return; + } + auto cameraForwardVector = RotateVector(forwardVector, cameraRotation); cameraForwardVector.Unitize(); @@ -1225,9 +1356,14 @@ namespace Hooks float pitch = a_this->data.angle.x * 0.5f; auto currentState = RE::PlayerCamera::GetSingleton()->currentState; - if (currentState && currentState->id == RE::CameraState::kMount) { - auto horseCameraState = static_cast(currentState.get()); - yaw += horseCameraState->freeRotation.x; + if (currentState) { + if (currentState->id == RE::CameraState::kMount) { + auto horseCameraState = static_cast(currentState.get()); + yaw += horseCameraState->freeRotation.x; + } else if (currentState->id == RE::CameraState::kDragon) { + auto dragonCameraState = static_cast(currentState.get()); + yaw += dragonCameraState->freeRotation.x; + } } yaw = NormalRelativeAngle(yaw); diff --git a/src/Hooks.h b/src/Hooks.h index a83d5a3..f394ad0 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -1,4 +1,5 @@ #pragma once +#include "DragonCameraState.h" namespace Hooks { @@ -143,6 +144,34 @@ namespace Hooks static inline REL::Relocation _ProcessButton; }; + class DragonCameraStateHook + { + public: + static void Hook() + { + REL::Relocation DragonCameraStateVtbl{ RE::VTABLE_DragonCameraState[0] }; + _OnEnterState = DragonCameraStateVtbl.write_vfunc(0x1, OnEnterState); + _OnExitState = DragonCameraStateVtbl.write_vfunc(0x2, OnExitState); + _UpdateRotation = DragonCameraStateVtbl.write_vfunc(0xE, UpdateRotation); + _HandleLookInput = DragonCameraStateVtbl.write_vfunc(0xF, HandleLookInput); + REL::Relocation PlayerInputHandlerVtbl{ RE::VTABLE_DragonCameraState[1] }; + _ProcessButton = PlayerInputHandlerVtbl.write_vfunc(0x4, ProcessButton); + } + + private: + static void OnEnterState(RE::DragonCameraState* a_this); + static void OnExitState(RE::DragonCameraState* a_this); + static void UpdateRotation(RE::DragonCameraState* a_this); + static void HandleLookInput(RE::DragonCameraState* a_this, const RE::NiPoint2& a_input); + static void ProcessButton(RE::DragonCameraState* a_this, RE::ButtonEvent* a_event, RE::PlayerControlsData* a_data); + + static inline REL::Relocation _OnEnterState; + static inline REL::Relocation _OnExitState; + static inline REL::Relocation _UpdateRotation; + static inline REL::Relocation _HandleLookInput; + static inline REL::Relocation _ProcessButton; + }; + class TweenMenuCameraStateHook { public: diff --git a/src/ModAPI.cpp b/src/ModAPI.cpp index 6fd4caf..d0e2275 100644 --- a/src/ModAPI.cpp +++ b/src/ModAPI.cpp @@ -31,6 +31,15 @@ bool Messaging::TDMInterface::GetTargetLockState() const noexcept return false; } +bool Messaging::TDMInterface::IsTargetLockBehindTarget() const noexcept +{ + auto directionalMovementHandler = DirectionalMovementHandler::GetSingleton(); + if (directionalMovementHandler) { + return directionalMovementHandler->IsTargetLockBehindTarget(); + } + return false; +} + RE::ActorHandle Messaging::TDMInterface::GetCurrentTarget() const noexcept { auto directionalMovementHandler = DirectionalMovementHandler::GetSingleton(); @@ -63,6 +72,29 @@ Messaging::APIResult Messaging::TDMInterface::RequestDisableDirectionalMovement( return APIResult::OK; } +Messaging::APIResult Messaging::TDMInterface::RequestDisableTargetLock(SKSE::PluginHandle a_modHandle) noexcept +{ + const auto owner = targetLockReticleOwner.load(std::memory_order::memory_order_acquire); + if (owner != SKSE::kInvalidPluginHandle) + if (owner == a_modHandle) + return APIResult::AlreadyGiven; + else + return APIResult::AlreadyTaken; + + if (needsTargetLockControl) + return APIResult::MustKeep; + auto expected = static_cast(SKSE::kInvalidPluginHandle); + if (!targetLockReticleOwner.compare_exchange_strong(expected, a_modHandle, std::memory_order::memory_order_acq_rel)) + return APIResult::AlreadyTaken; + + auto directionalMovementHandler = DirectionalMovementHandler::GetSingleton(); + if (directionalMovementHandler) { + directionalMovementHandler->SetForceDisableTargetLock(true); + } + + return APIResult::OK; +} + Messaging::APIResult Messaging::TDMInterface::RequestDisableHeadtracking(SKSE::PluginHandle a_modHandle) noexcept { const auto owner = headtrackingOwner.load(std::memory_order::memory_order_acquire); @@ -91,6 +123,11 @@ SKSE::PluginHandle Messaging::TDMInterface::GetDisableDirectionalMovementOwner() return directionalMovementOwner; } +SKSE::PluginHandle Messaging::TDMInterface::GetDisableTargetLockOwner() const noexcept +{ + return targetLockReticleOwner; +} + SKSE::PluginHandle Messaging::TDMInterface::GetDisableHeadtrackingOwner() const noexcept { return headtrackingOwner; @@ -110,6 +147,19 @@ Messaging::APIResult Messaging::TDMInterface::ReleaseDisableDirectionalMovement( return APIResult::OK; } +Messaging::APIResult Messaging::TDMInterface::ReleaseDisableTargetLock(SKSE::PluginHandle a_modHandle) noexcept +{ + if (targetLockReticleOwner != a_modHandle) + return APIResult::NotOwner; + targetLockReticleOwner.store(SKSE::kInvalidPluginHandle, std::memory_order::memory_order_release); + auto directionalMovementHandler = DirectionalMovementHandler::GetSingleton(); + if (directionalMovementHandler) { + directionalMovementHandler->SetForceDisableTargetLock(false); + } + + return APIResult::OK; +} + Messaging::APIResult Messaging::TDMInterface::ReleaseDisableHeadtracking(SKSE::PluginHandle a_modHandle) noexcept { if (headtrackingOwner != a_modHandle) @@ -191,6 +241,11 @@ void Messaging::TDMInterface::SetNeedsDirectionalMovementControl(bool a_needsCon needsDirectionalMovementControl = a_needsControl; } +void Messaging::TDMInterface::SetNeedsTargetLockControl(bool a_needsControl) noexcept +{ + needsTargetLockControl = a_needsControl; +} + void Messaging::TDMInterface::SetNeedsHeadtrackingControl(bool a_needsControl) noexcept { needsHeadtrackingControl = a_needsControl; @@ -206,6 +261,11 @@ bool Messaging::TDMInterface::IsDirectionalMovementControlTaken() const noexcept return directionalMovementOwner.load(std::memory_order::memory_order_acquire) != SKSE::kInvalidPluginHandle; } +bool Messaging::TDMInterface::IsTargetLockControlTaken() const noexcept +{ + return targetLockReticleOwner.load(std::memory_order::memory_order_acquire) != SKSE::kInvalidPluginHandle; +} + bool Messaging::TDMInterface::IsHeadtrackingControlTaken() const noexcept { return headtrackingOwner.load(std::memory_order::memory_order_acquire) != SKSE::kInvalidPluginHandle; diff --git a/src/ModAPI.h b/src/ModAPI.h index 356824b..e8333da 100644 --- a/src/ModAPI.h +++ b/src/ModAPI.h @@ -9,8 +9,10 @@ namespace Messaging using InterfaceVersion1 = ::TDM_API::IVTDM1; using InterfaceVersion2 = ::TDM_API::IVTDM2; using InterfaceVersion3 = ::TDM_API::IVTDM3; + using InterfaceVersion4 = ::TDM_API::IVTDM4; + using InterfaceVersion5 = ::TDM_API::IVTDM5; - class TDMInterface : public InterfaceVersion3 + class TDMInterface : public InterfaceVersion5 { private: TDMInterface() noexcept; @@ -44,9 +46,19 @@ namespace Messaging virtual TDM_API::DirectionalMovementMode GetDirectionalMovementMode() const noexcept override; virtual RE::NiPoint2 GetActualMovementInput() const noexcept override; + // InterfaceVersion4 + virtual bool IsTargetLockBehindTarget() const noexcept override; + + // InterfaceVersion5 + virtual APIResult RequestDisableTargetLock(SKSE::PluginHandle a_modHandle) noexcept override; + virtual SKSE::PluginHandle GetDisableTargetLockOwner() const noexcept override; + virtual APIResult ReleaseDisableTargetLock(SKSE::PluginHandle a_modHandle) noexcept override; + // Internal // Mark directional movement control as required by True Directional Movement for API requests void SetNeedsDirectionalMovementControl(bool a_needsControl) noexcept; + // Mark target lock reticle control as required by True Directional Movement for API requests + void SetNeedsTargetLockControl(bool a_needsControl) noexcept; // Mark headtracking control as required by True Directional Movement for API requests void SetNeedsHeadtrackingControl(bool a_needsControl) noexcept; // Mark player yaw control as required by True Directional Movement for API requests @@ -54,6 +66,8 @@ namespace Messaging // Does a mod have control over the directional movement? bool IsDirectionalMovementControlTaken() const noexcept; + // Does a mod have control over the target lock reticle? + bool IsTargetLockControlTaken() const noexcept; // Does a mod have control over the headtracking? bool IsHeadtrackingControlTaken() const noexcept; // Does a mod have control over the player character's yaw? @@ -65,6 +79,9 @@ namespace Messaging bool needsDirectionalMovementControl = false; std::atomic directionalMovementOwner = SKSE::kInvalidPluginHandle; + bool needsTargetLockControl = false; + std::atomic targetLockReticleOwner = SKSE::kInvalidPluginHandle; + bool needsHeadtrackingControl = false; std::atomic headtrackingOwner = SKSE::kInvalidPluginHandle; diff --git a/src/Papyrus.cpp b/src/Papyrus.cpp index ea3d21d..f9d11f0 100644 --- a/src/Papyrus.cpp +++ b/src/Papyrus.cpp @@ -46,6 +46,15 @@ namespace Papyrus DirectionalMovementHandler::GetSingleton()->PapyrusDisableDirectionalMovement(a_modName, a_bDisable); } + void TrueDirectionalMovement::ToggleDisableTargetLock(RE::StaticFunctionTag*, RE::BSFixedString a_modName, bool a_bDisable) + { + if (a_modName.empty()) { + return; + } + + DirectionalMovementHandler::GetSingleton()->PapyrusDisableTargetLock(a_modName, a_bDisable); + } + void TrueDirectionalMovement::ToggleDisableHeadtracking(RE::StaticFunctionTag*, RE::BSFixedString a_modName, bool a_bDisable) { if (a_modName.empty()) { @@ -61,6 +70,7 @@ namespace Papyrus a_vm->RegisterFunction("GetTargetLockState", "TrueDirectionalMovement", GetTargetLockState); a_vm->RegisterFunction("GetCurrentTarget", "TrueDirectionalMovement", GetCurrentTarget); a_vm->RegisterFunction("ToggleDisableDirectionalMovement", "TrueDirectionalMovement", ToggleDisableDirectionalMovement); + a_vm->RegisterFunction("ToggleDisableTargetLock", "TrueDirectionalMovement", ToggleDisableTargetLock); a_vm->RegisterFunction("ToggleDisableHeadtracking", "TrueDirectionalMovement", ToggleDisableHeadtracking); logger::info("Registered TrueDirectionalMovement class"); diff --git a/src/Papyrus.h b/src/Papyrus.h index 7b9964c..f4ab0c1 100644 --- a/src/Papyrus.h +++ b/src/Papyrus.h @@ -17,6 +17,7 @@ namespace Papyrus static bool GetTargetLockState(RE::StaticFunctionTag*); static RE::Actor* GetCurrentTarget(RE::StaticFunctionTag*); static void ToggleDisableDirectionalMovement(RE::StaticFunctionTag*, RE::BSFixedString a_modName, bool a_bDisable); + static void ToggleDisableTargetLock(RE::StaticFunctionTag*, RE::BSFixedString a_modName, bool a_bDisable); static void ToggleDisableHeadtracking(RE::StaticFunctionTag*, RE::BSFixedString a_modName, bool a_bDisable); static bool Register(RE::BSScript::IVirtualMachine* a_vm); diff --git a/src/Settings.cpp b/src/Settings.cpp index 96f545a..ddd4b5c 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -150,6 +150,7 @@ void Settings::ReadSettings() ReadFloatSetting(mcm, "TargetLock", "fTargetLockPitchAdjustSpeed", fTargetLockPitchAdjustSpeed); ReadFloatSetting(mcm, "TargetLock", "fTargetLockYawAdjustSpeed", fTargetLockYawAdjustSpeed); ReadFloatSetting(mcm, "TargetLock", "fTargetLockPitchOffsetStrength", fTargetLockPitchOffsetStrength); + ReadFloatSetting(mcm, "TargetLock", "fTargetLockMinHeightAboveGround", fTargetLockMinHeightAboveGround); ReadUInt32Setting(mcm, "TargetLock", "uTargetLockArrowAimType", (uint32_t&)uTargetLockArrowAimType); ReadUInt32Setting(mcm, "TargetLock", "uTargetLockMissileAimType", (uint32_t&)uTargetLockMissileAimType); ReadBoolSetting(mcm, "TargetLock", "bTargetLockUsePOVSwitchKeyboard", bTargetLockUsePOVSwitchKeyboard); @@ -161,6 +162,7 @@ void Settings::ReadSettings() ReadBoolSetting(mcm, "TargetLock", "bTargetLockUseRightThumbstick", bTargetLockUseRightThumbstick); ReadBoolSetting(mcm, "TargetLock", "bResetCameraWithTargetLock", bResetCameraWithTargetLock); ReadBoolSetting(mcm, "TargetLock", "bResetCameraPitch", bResetCameraPitch); + ReadBoolSetting(mcm, "TargetLock", "bTargetLockEnableLockBehindTarget", bTargetLockEnableLockBehindTarget); // HUD ReadBoolSetting(mcm, "HUD", "bEnableTargetLockReticle", bEnableTargetLockReticle); @@ -185,6 +187,7 @@ void Settings::ReadSettings() ReadUInt32Setting(mcm, "Keys", "uTargetLockKey", uTargetLockKey); ReadUInt32Setting(mcm, "Keys", "uSwitchTargetLeftKey", uSwitchTargetLeftKey); ReadUInt32Setting(mcm, "Keys", "uSwitchTargetRightKey", uSwitchTargetRightKey); + ReadUInt32Setting(mcm, "Keys", "uTargetLockBehindTargetKey", uTargetLockBehindTargetKey); }; logger::info("Reading MCM .ini..."); diff --git a/src/Settings.h b/src/Settings.h index 44b1b01..c7e4347 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -131,6 +131,11 @@ struct Settings static inline bool bTargetLockUseRightThumbstick = true; static inline bool bResetCameraWithTargetLock = true; static inline bool bResetCameraPitch = false; + static inline bool bTargetLockConsiderGroundLevel = true; + static inline float fTargetLockMinHeightAboveGround = 35.f; + static inline bool bTargetLockEnableLockBehindTarget = false; + static inline float fCameraBehindTargetMinDistance = 50.f; + static inline float fCameraBehindTargetNoSwitchRange = 150.f; // HUD static inline bool bEnableTargetLockReticle = true; @@ -155,6 +160,7 @@ struct Settings static inline uint32_t uTargetLockKey = 258; static inline uint32_t uSwitchTargetLeftKey = static_cast(-1); static inline uint32_t uSwitchTargetRightKey = static_cast(-1); + static inline uint32_t uTargetLockBehindTargetKey = static_cast(-1); // Non-MCM static inline std::unordered_map> targetPoints; diff --git a/src/TrueDirectionalMovementAPI.h b/src/TrueDirectionalMovementAPI.h index 7658f27..15db4d2 100644 --- a/src/TrueDirectionalMovementAPI.h +++ b/src/TrueDirectionalMovementAPI.h @@ -17,7 +17,9 @@ { V1, V2, - V3 + V3, + V4, + V5 }; // Error types that may be returned by the True Directional Movement API @@ -178,6 +180,41 @@ [[nodiscard]] virtual RE::NiPoint2 GetActualMovementInput() const noexcept = 0; }; + class IVTDM4 : public IVTDM3 + { + public: + /// + /// Check if the target position for the camera is behind the target when target lock is enabled. + /// + /// true if target lock is enabled AND the target position is behind the camera. False otherwise. + [[nodiscard]] virtual bool IsTargetLockBehindTarget() const noexcept = 0; + }; + + class IVTDM5 : public IVTDM4 + { + public: + /// + /// Request the plugin to forcibly disable target lock reticle. + /// If granted, target lock reticle will be disabled for the duration of your control. + /// + /// Your assigned plugin handle + /// OK, MustKeep, AlreadyGiven, AlreadyTaken + [[nodiscard]] virtual APIResult RequestDisableTargetLock(PluginHandle a_myPluginHandle) noexcept = 0; + + /// + /// Release your forced disable of target lock reticle. + /// + /// Your assigned plugin handle + /// OK, NotOwner + virtual APIResult ReleaseDisableTargetLock(PluginHandle a_myPluginHandle) noexcept = 0; + + /// + /// Returns the current owner of the forced disable of target lock reticle. + /// + /// Handle or kPluginHandle_Invalid if no one currently owns the resource + virtual PluginHandle GetDisableTargetLockOwner() const noexcept = 0; + }; + typedef void* (*_RequestPluginAPI)(const InterfaceVersion interfaceVersion); /// @@ -186,7 +223,7 @@ /// /// The interface version to request /// The pointer to the API singleton, or nullptr if request failed - [[nodiscard]] inline void* RequestPluginAPI(const InterfaceVersion a_interfaceVersion = InterfaceVersion::V3) + [[nodiscard]] inline void* RequestPluginAPI(const InterfaceVersion a_interfaceVersion = InterfaceVersion::V5) { auto pluginHandle = GetModuleHandle("TrueDirectionalMovement.dll"); _RequestPluginAPI requestAPIFunction = (_RequestPluginAPI)GetProcAddress(pluginHandle, "RequestPluginAPI"); diff --git a/src/Utils.cpp b/src/Utils.cpp index 9862ee2..c8b00a3 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -34,7 +34,8 @@ RE::NiPoint3 GetCameraPos() if (playerCamera->currentState == playerCamera->cameraStates[RE::CameraStates::kFirstPerson] || playerCamera->currentState == playerCamera->cameraStates[RE::CameraStates::kThirdPerson] || - playerCamera->currentState == playerCamera->cameraStates[RE::CameraStates::kMount]) { + playerCamera->currentState == playerCamera->cameraStates[RE::CameraStates::kMount] || + playerCamera->currentState == playerCamera->cameraStates[RE::CameraStates::kDragon]) { RE::NiNode* root = playerCamera->cameraRoot.get(); if (root) { ret.x = root->world.translate.x; @@ -234,3 +235,52 @@ bool PredictAimProjectile(RE::NiPoint3 a_projectilePos, RE::NiPoint3 a_targetPos return bValidSolutionFound; } + +float GetLandHeightWithWater(RE::NiPoint3 a_pos) +{ + /* Extension of function GetLandHeight() from PO3_SKSEFunctions*/ + float heightOut = -1; + + if (auto TES = RE::TES::GetSingleton()) { + TES->GetLandHeight(a_pos, heightOut); + + auto playerCharacter = RE::PlayerCharacter::GetSingleton(); + + auto cell = playerCharacter->GetParentCell(); + auto waterHeight = !cell || cell == playerCharacter->parentCell ? playerCharacter->GetWaterHeight() : cell->GetExteriorWaterHeight(); + + if (waterHeight == -FLT_MAX && cell) { + waterHeight = cell->GetExteriorWaterHeight(); + } + + if (heightOut < waterHeight) { + heightOut = waterHeight; + } + } + + return heightOut; +} + +RE::TESCondition* condition_GetFlyingState; +int GetFlyingState(RE::Actor* a_akActor) { + if (!a_akActor) { + logger::warn("{}: error, a_akActor doesn't exist", __func__); + return -1; + } + + if (!condition_GetFlyingState) { + auto* conditionItem = new RE::TESConditionItem; + conditionItem->data.functionData.function = RE::FUNCTION_DATA::FunctionID::kGetFlyingState; + + condition_GetFlyingState = new RE::TESCondition; + condition_GetFlyingState->head = conditionItem; + } + + for (int i = 0; i < 6; i++) { + condition_GetFlyingState->head->data.comparisonValue.f = static_cast(i); + if (condition_GetFlyingState->IsTrue(a_akActor, nullptr)) { + return i; + } + } + return -1; +} diff --git a/src/Utils.h b/src/Utils.h index 20c444d..b4dbee9 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -26,6 +26,8 @@ bool GetTargetPointPosition(RE::ObjectRefHandle a_target, std::string_view a_tar void SetRotationMatrix(RE::NiMatrix3& a_matrix, float sacb, float cacb, float sb); bool PredictAimProjectile(RE::NiPoint3 a_projectilePos, RE::NiPoint3 a_targetPosition, RE::NiPoint3 a_targetVelocity, float a_gravity, RE::NiPoint3& a_projectileVelocity); +float GetLandHeightWithWater(RE::NiPoint3 a_pos); +int GetFlyingState(RE::Actor* a_akActor); [[nodiscard]] inline float GetPlayerTimeMultiplier() { @@ -276,3 +278,9 @@ bool PredictAimProjectile(RE::NiPoint3 a_projectilePos, RE::NiPoint3 a_targetPos { return (((a_oldValue - a_oldMin) * (a_newMax - a_newMin)) / (a_oldMax - a_oldMin)) + a_newMin; } + +[[nodiscard]] inline float GetYaw(const RE::NiQuaternion a_rotation) +{ + // will not produce reliable results near the gimbal lock (pitch approaching +/- PI/2, ie straight upwards or downwards pitch) + return std::atan2(2.0f * (a_rotation.w * a_rotation.z + a_rotation.x * a_rotation.y), 1.0f - 2.0f * (a_rotation.y * a_rotation.y + a_rotation.z * a_rotation.z)); +} diff --git a/src/main.cpp b/src/main.cpp index 33b0403..7f10a9d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -67,7 +67,7 @@ namespace spdlog::set_pattern("%g(%#): [%^%l%$] %v"s); } } - +/* extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Query(const SKSE::QueryInterface* a_skse, SKSE::PluginInfo* a_info) { a_info->infoVersion = SKSE::PluginInfo::kVersion; @@ -100,11 +100,19 @@ extern "C" DLLEXPORT constinit auto SKSEPlugin_Version = []() { return v; }(); +*/ +SKSEPluginInfo( + .Version = Plugin::VERSION, + .Name = Plugin::NAME, + .Author = "Ersh", + .RuntimeCompatibility = SKSE::PluginDeclaration::RuntimeCompatibility(SKSE::VersionIndependence::AddressLibrary), + .MinimumSKSEVersion = { 2, 2, 3 } // or 0 if you want to support all +) extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) { #ifndef NDEBUG - while (!IsDebuggerPresent()) { Sleep(100); } +// while (!IsDebuggerPresent()) { Sleep(100); } #endif REL::Module::reset(); // Clib-NG bug workaround @@ -137,6 +145,10 @@ extern "C" DLLEXPORT void* SKSEAPI RequestPluginAPI(const TDM_API::InterfaceVers case TDM_API::InterfaceVersion::V2: [[fallthrough]]; case TDM_API::InterfaceVersion::V3: + [[fallthrough]]; + case TDM_API::InterfaceVersion::V4: + [[fallthrough]]; + case TDM_API::InterfaceVersion::V5: logger::info("TrueDirectionalMovement::RequestPluginAPI returned the API singleton"); return static_cast(api); } diff --git a/translation/TrueDirectionalMovement_english.txt b/translation/TrueDirectionalMovement_english.txt index 2b75410..ac266cd 100644 Binary files a/translation/TrueDirectionalMovement_english.txt and b/translation/TrueDirectionalMovement_english.txt differ