From 3ecaaf212a0e6ece09238b16e85d5ff3b02fc1b8 Mon Sep 17 00:00:00 2001 From: zspatter <33292185+zspatter@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:53:59 -0400 Subject: [PATCH 1/2] Support cone projectiles in target lock aim types Cone-type projectiles were never redirected during target lock: the GetLinearVelocity hook was not installed on ConeProjectile's vtable, and ProjectileAimSupport's form type switch fell through to free aim for them. Install the hook and map cones to the spell/missile aim type setting so predict and homing work for cone projectiles the same way they do for missiles. --- src/Hooks.cpp | 10 ++++++++++ src/Hooks.h | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/src/Hooks.cpp b/src/Hooks.cpp index a135a9a..6a57679 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -948,6 +948,9 @@ namespace Hooks case RE::FormType::ProjectileMissile: aimType = Settings::uTargetLockMissileAimType; break; + case RE::FormType::ProjectileCone: + aimType = Settings::uTargetLockMissileAimType; + break; default: aimType = TargetLockProjectileAimType::kFreeAim; } @@ -1070,6 +1073,13 @@ namespace Hooks ProjectileAimSupport(a_this); } + void ProjectileHook::GetLinearVelocityCone(RE::Projectile* a_this, RE::NiPoint3& a_outVelocity) + { + _GetLinearVelocityCone(a_this, a_outVelocity); + + ProjectileAimSupport(a_this); + } + bool ProjectileHook::Func183(RE::Projectile* a_this) { // player only, 0x100000 == player diff --git a/src/Hooks.h b/src/Hooks.h index f394ad0..7101ccf 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -243,9 +243,11 @@ namespace Hooks REL::Relocation ArrowProjectileVtbl{ RE::VTABLE_ArrowProjectile[0] }; // 1676318 REL::Relocation MissileProjectileVtbl{ RE::VTABLE_MissileProjectile[0] }; // 167AE78 REL::Relocation BeamProjectileVtbl{ RE::VTABLE_BeamProjectile[0] }; // 1677660 + REL::Relocation ConeProjectileVtbl{ RE::VTABLE_ConeProjectile[0] }; _GetLinearVelocityProjectile = ProjectileVtbl.write_vfunc(0x86, GetLinearVelocityProjectile); _GetLinearVelocityArrow = ArrowProjectileVtbl.write_vfunc(0x86, GetLinearVelocityArrow); _GetLinearVelocityMissile = MissileProjectileVtbl.write_vfunc(0x86, GetLinearVelocityMissile); + _GetLinearVelocityCone = ConeProjectileVtbl.write_vfunc(0x86, GetLinearVelocityCone); auto& trampoline = SKSE::GetTrampoline(); REL::Relocation hook{ RELOCATION_ID(43030, 44222) }; // 754820, 7821A0 @@ -258,12 +260,14 @@ namespace Hooks static void GetLinearVelocityProjectile(RE::Projectile* a_this, RE::NiPoint3& a_outVelocity); static void GetLinearVelocityArrow(RE::Projectile* a_this, RE::NiPoint3& a_outVelocity); static void GetLinearVelocityMissile(RE::Projectile* a_this, RE::NiPoint3& a_outVelocity); + static void GetLinearVelocityCone(RE::Projectile* a_this, RE::NiPoint3& a_outVelocity); static bool Func183(RE::Projectile* a_this); static void InitProjectile(RE::Projectile* a_this); static inline REL::Relocation _GetLinearVelocityProjectile; static inline REL::Relocation _GetLinearVelocityArrow; static inline REL::Relocation _GetLinearVelocityMissile; + static inline REL::Relocation _GetLinearVelocityCone; static inline REL::Relocation _InitProjectile; }; From b4858917efea7c2e205772dee01e959ba11cdde3 Mon Sep 17 00:00:00 2001 From: zspatter <33292185+zspatter@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:55:02 -0400 Subject: [PATCH 2/2] Aim player cone projectiles at the crosshair in third person The engine launches cone projectiles using the shooter's body pitch rather than the camera aim pitch. With the free camera active, the character's pitch is not synced to the camera (UpdateFacingCrosshair only transfers yaw), so player-fired cone projectiles miss the crosshair, with the error growing with distance. Missiles, arrows and beams are unaffected because their launch direction is derived from the camera. Re-aim cone projectiles along the camera-crosshair raycast at launch, using the same approach as the existing mounted aiming correction in InitProjectile. The arrow tilt-up angle is intentionally not applied since cones receive no engine side crosshair correction that would compensate for it. --- src/Hooks.cpp | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 6a57679..ad060de 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -1224,6 +1224,93 @@ namespace Hooks RE::NiPoint3 rightVector = direction.Cross(upVector); direction = RotateAngleAxis(direction, AngleToRadian(*g_f3PArrowTiltUpAngle), rightVector); + linearVelocity = direction * velocityScalar; + } + } else if (a_this->formType.get() == RE::FormType::ProjectileCone && directionalMovementHandler->IsFreeCamera() && + (!directionalMovementHandler->HasTargetLocked() || Settings::uTargetLockMissileAimType == TargetLockProjectileAimType::kFreeAim)) { + // The game launches cone projectiles with the shooter's body pitch instead of the camera aim pitch, + // so they miss the crosshair while the third person free camera is active (the character's pitch is + // not synced to the camera). Re-aim them along the camera ray. Only relevant for long range cone + // projectiles used by some spell mods, e.g. Astral Magic 2. + auto playerCamera = RE::PlayerCamera::GetSingleton(); + if (playerCamera->currentState && playerCamera->currentState->id == RE::CameraState::kThirdPerson) { + constexpr RE::NiPoint3 forwardVector{ 0.f, 1.f, 0.f }; + auto thirdPersonState = static_cast(playerCamera->currentState.get()); + RE::NiQuaternion cameraRotation; + thirdPersonState->GetRotation(cameraRotation); + + auto cameraForwardVector = RotateVector(forwardVector, cameraRotation); + + cameraForwardVector.Unitize(); + + auto cameraPos = playerCamera->cameraRoot->world.translate; + + RE::NiPoint3 rayStart = cameraPos; + RE::NiPoint3 rayEnd = cameraPos + cameraForwardVector * 5000.f; + RE::NiPoint3 hitPos = rayEnd; + + RE::NiPoint3 cameraToPlayer = playerCamera->cameraTarget.get()->GetPosition() - cameraPos; + RE::NiPoint3 cameraToTarget = rayEnd - cameraPos; + RE::NiPoint3 projected = Project(cameraToPlayer, cameraToTarget); + RE::NiPoint3 projectedPos = RE::NiPoint3(projected.x + cameraPos.x, projected.y + cameraPos.y, projected.z + cameraPos.z); + rayStart = projectedPos; + + uint16_t playerCollisionGroup = 0; + + auto playerCharacter = RE::PlayerCharacter::GetSingleton(); + if (auto playerBody = playerCharacter->Get3D()) { + if (auto collisionObject = playerBody->GetCollisionObject()) { + if (auto rigidBody = collisionObject->GetRigidBody()) { + playerCollisionGroup = static_cast(rigidBody->referencedObject.get())->collidable.broadPhaseHandle.collisionFilterInfo >> 16; + } + } + } + + float bhkWorldScale = *g_worldScale; + + collector.Reset(); + RE::hkpWorldRayCastInput raycastInput; + raycastInput.filterInfo = ((uint32_t)playerCollisionGroup << 16) | 0x28; + raycastInput.from.quad = _mm_setr_ps(rayStart.x * bhkWorldScale, rayStart.y * bhkWorldScale, rayStart.z * bhkWorldScale, 0.f); + raycastInput.to.quad = _mm_setr_ps(rayEnd.x * bhkWorldScale, rayEnd.y * bhkWorldScale, rayEnd.z * bhkWorldScale, 0.f); + auto world = playerCharacter->parentCell->GetbhkWorld(); + world->worldLock.LockForRead(); + CastRay(world->GetWorld2(), raycastInput, collector); + world->worldLock.UnlockForRead(); + if (collector.doesHitExist) { + auto distance = rayEnd - rayStart; + hitPos = rayStart + (distance * collector.closestHitInfo.hitFraction); + } + + RE::NiPoint3 direction = hitPos - a_this->data.location; + direction.Unitize(); + + float rotationX, rotationZ; + + rotationX = atan2(-direction.z, std::sqrtf(direction.x * direction.x + direction.y * direction.y)); + rotationZ = atan2(direction.x, direction.y); + + if (rotationZ < 0.0) { + rotationZ += PI; + } + + if (direction.x < 0.0) { + rotationZ += PI; + } + + a_this->data.angle.x = rotationX; + a_this->data.angle.z = rotationZ; + + auto projectileNode = a_this->Get3D(); + if (projectileNode) { + SetRotationMatrix(projectileNode->local.rotate, direction.x, direction.y, direction.z); + } + + // no f3PArrowTiltUpAngle tilt here: unlike arrows, cone projectiles get no engine side + // crosshair correction that would compensate for it, so the exact ray direction is what we want + auto& linearVelocity = a_this->GetProjectileRuntimeData().linearVelocity; + float velocityScalar = linearVelocity.Length(); + linearVelocity = direction * velocityScalar; } }