From fbc8195bed49c69f68c633022277a2b042957e8e Mon Sep 17 00:00:00 2001 From: Federico Romero Date: Thu, 4 Jun 2026 20:11:06 -0300 Subject: [PATCH] Add per-weapon aim camera offset and zoom overrides Introduces six client-side Lua functions that allow scripts to shift the on-foot aim camera per weapon (shoulder cam) and override the FOV used while aiming: setWeaponAimCameraOffset / getWeaponAimCameraOffset / resetWeaponAimCameraOffset setWeaponAimCameraZoom / getWeaponAimCameraZoom / resetWeaponAimCameraZoom The implementation hooks the CALL to CCam::Process_AimWeapon at 0x527A95, applies a frame-rate-independent blend (~0.15s) on top of the game's computed Source/Front/Up vectors, then patches FOV when set. All state lives in CCameraSA and is cleared on server connect via ResetAllWeaponAimCameraOverrides(), called from CClientGame::Event_OnIngame. --- Client/game_sa/CCameraSA.cpp | 134 ++++++++++++++++++ Client/game_sa/CCameraSA.h | 8 ++ Client/mods/deathmatch/logic/CClientGame.cpp | 1 + .../logic/luadefs/CLuaCameraDefs.cpp | 65 +++++++++ .../deathmatch/logic/luadefs/CLuaCameraDefs.h | 8 ++ Client/sdk/game/CCamera.h | 10 ++ 6 files changed, 226 insertions(+) diff --git a/Client/game_sa/CCameraSA.cpp b/Client/game_sa/CCameraSA.cpp index 831e6b56aca..c41050fdd3b 100644 --- a/Client/game_sa/CCameraSA.cpp +++ b/Client/game_sa/CCameraSA.cpp @@ -12,6 +12,7 @@ #include "StdInc.h" #include "CCameraSA.h" #include "CGameSA.h" +#include "CPedSA.h" #include #include #include @@ -36,6 +37,79 @@ namespace { return std::isfinite(vec.fX) && std::isfinite(vec.fY) && std::isfinite(vec.fZ); } + + // Per-weapon aiming camera state. A zero offset or zero zoom means disabled. + CVector s_aimOffset[WEAPONTYPE_LAST_WEAPONTYPE]{}; + float s_aimZoom[WEAPONTYPE_LAST_WEAPONTYPE]{}; + + // Blend-in state: ramps from 0 to 1 over ~0.15s when entering aim mode. + float s_aimBlend = 0.0f; + int s_lastAimFrame = -999; + constexpr float kAimBlendSpeed = 1.0f / (0.15f * 30.0f); + + constexpr DWORD HOOKSITE_AimWeapon = 0x527A95; // CALL CCam::Process_AimWeapon + constexpr DWORD FUNC_AimWeapon_Orig = 0x521500; + + static void __fastcall Hook_Process_AimWeapon(CCamSAInterface* cam, int /*edx*/, const CVector& vec, float arg3, float arg4, float arg5) + { + using Orig_t = void(__thiscall*)(CCamSAInterface*, const CVector&, float, float, float); + static const auto s_orig = reinterpret_cast(FUNC_AimWeapon_Orig); + + // Let GTA compute Source/Front/Up first, then layer overrides on top. + s_orig(cam, vec, arg3, arg4, arg5); + + using FindPed_t = CPedSAInterface*(__cdecl*)(int); + static const auto s_findPed = reinterpret_cast(0x56E210); + + CPedSAInterface* pPed = s_findPed(-1); + if (!pPed || pPed->pedFlags.bInVehicle) + return; + + const std::uint8_t slot = pPed->bCurrentWeaponSlot; + if (slot >= WEAPONSLOT_MAX) + return; + + const eWeaponType wtype = pPed->Weapons[slot].m_eWeaponType; + if (wtype <= WEAPONTYPE_UNARMED || wtype >= WEAPONTYPE_LAST_WEAPONTYPE) + return; + + const CVector& off = s_aimOffset[wtype]; + const float zoom = s_aimZoom[wtype]; + const bool hasOffset = (off.fX != 0.0f || off.fY != 0.0f || off.fZ != 0.0f); + const bool hasZoom = (zoom > 0.0f); + + if (!hasOffset && !hasZoom) + return; + + // CTimer::m_FrameCounter at 0xB7CB4C, ms_fTimeStep at 0xB7CB5C. + const int curFrame = *(const int*)0xB7CB4C; + const float timeStep = *(const float*)0xB7CB5C; + + // Reset the blend if the hook went idle for more than one frame. + if (curFrame > s_lastAimFrame + 2) + s_aimBlend = 0.0f; + s_lastAimFrame = curFrame; + + s_aimBlend = std::min(1.0f, s_aimBlend + kAimBlendSpeed * timeStep); + + if (hasOffset) + { + // Right = Front cross Up + CVector right; + right.fX = cam->Front.fY * cam->Up.fZ - cam->Front.fZ * cam->Up.fY; + right.fY = cam->Front.fZ * cam->Up.fX - cam->Front.fX * cam->Up.fZ; + right.fZ = cam->Front.fX * cam->Up.fY - cam->Front.fY * cam->Up.fX; + + const float b = s_aimBlend; + cam->Source.fX += (right.fX * off.fX + cam->Up.fX * off.fY + cam->Front.fX * off.fZ) * b; + cam->Source.fY += (right.fY * off.fX + cam->Up.fY * off.fY + cam->Front.fY * off.fZ) * b; + cam->Source.fZ += (right.fZ * off.fX + cam->Up.fZ * off.fY + cam->Front.fZ * off.fZ) * b; + cam->SourceBeforeLookBehind = cam->Source; + } + + if (hasZoom) + cam->FOV += (zoom - cam->FOV) * s_aimBlend; + } } extern CGameSA* pGame; @@ -92,6 +166,9 @@ CCameraSA::CCameraSA(CCameraSAInterface* cameraInterface) s_cameraClipMask.store(static_cast(CameraClipFlags::Objects) | static_cast(CameraClipFlags::Vehicles), std::memory_order_relaxed); HookInstall(HOOKPOS_Camera_CollisionDetection, (DWORD)HOOK_Camera_CollisionDetection, 5); + + // Redirect the CALL at HOOKSITE_AimWeapon (E8 opcode + 4-byte relative offset). + MemPut(HOOKSITE_AimWeapon + 1, reinterpret_cast(&Hook_Process_AimWeapon) - (HOOKSITE_AimWeapon + 5)); } CCameraSA::~CCameraSA() @@ -783,3 +860,60 @@ bool CCameraSA::IsSphereVisible(CVector* center, float radius) const return ((bool(__thiscall*)(CCameraSAInterface*, CVector*, float))0x420D40)(cameraInterface, center, radius); } + +void CCameraSA::SetWeaponAimCameraOffset(eWeaponType weaponType, float fX, float fY, float fZ) +{ + if (weaponType > WEAPONTYPE_UNARMED && weaponType < WEAPONTYPE_LAST_WEAPONTYPE) + s_aimOffset[weaponType] = CVector(fX, fY, fZ); +} + +void CCameraSA::GetWeaponAimCameraOffset(eWeaponType weaponType, float& fX, float& fY, float& fZ) +{ + if (weaponType > WEAPONTYPE_UNARMED && weaponType < WEAPONTYPE_LAST_WEAPONTYPE) + { + fX = s_aimOffset[weaponType].fX; + fY = s_aimOffset[weaponType].fY; + fZ = s_aimOffset[weaponType].fZ; + } + else + { + fX = fY = fZ = 0.0f; + } +} + +void CCameraSA::ResetWeaponAimCameraOffset(eWeaponType weaponType) +{ + if (weaponType > WEAPONTYPE_UNARMED && weaponType < WEAPONTYPE_LAST_WEAPONTYPE) + s_aimOffset[weaponType] = CVector(0.0f, 0.0f, 0.0f); +} + +void CCameraSA::SetWeaponAimCameraZoom(eWeaponType weaponType, float fFOV) +{ + // FOV outside (0, 180) would produce a degenerate projection matrix. + if (weaponType > WEAPONTYPE_UNARMED && weaponType < WEAPONTYPE_LAST_WEAPONTYPE && fFOV > 0.0f && fFOV < 180.0f) + s_aimZoom[weaponType] = fFOV; +} + +float CCameraSA::GetWeaponAimCameraZoom(eWeaponType weaponType) +{ + if (weaponType > WEAPONTYPE_UNARMED && weaponType < WEAPONTYPE_LAST_WEAPONTYPE) + return s_aimZoom[weaponType]; + return 0.0f; +} + +void CCameraSA::ResetWeaponAimCameraZoom(eWeaponType weaponType) +{ + if (weaponType > WEAPONTYPE_UNARMED && weaponType < WEAPONTYPE_LAST_WEAPONTYPE) + s_aimZoom[weaponType] = 0.0f; +} + +void CCameraSA::ResetAllWeaponAimCameraOverrides() +{ + for (int i = 0; i < WEAPONTYPE_LAST_WEAPONTYPE; ++i) + { + s_aimOffset[i] = CVector(0.0f, 0.0f, 0.0f); + s_aimZoom[i] = 0.0f; + } + s_aimBlend = 0.0f; + s_lastAimFrame = -999; +} diff --git a/Client/game_sa/CCameraSA.h b/Client/game_sa/CCameraSA.h index d65eea84f45..72ea52b25ba 100644 --- a/Client/game_sa/CCameraSA.h +++ b/Client/game_sa/CCameraSA.h @@ -425,4 +425,12 @@ class CCameraSA : public CCamera // Additional methods void RestoreLastGoodState(); + + void SetWeaponAimCameraOffset(eWeaponType weaponType, float fX, float fY, float fZ) override; + void GetWeaponAimCameraOffset(eWeaponType weaponType, float& fX, float& fY, float& fZ) override; + void ResetWeaponAimCameraOffset(eWeaponType weaponType) override; + void SetWeaponAimCameraZoom(eWeaponType weaponType, float fFOV) override; + float GetWeaponAimCameraZoom(eWeaponType weaponType) override; + void ResetWeaponAimCameraZoom(eWeaponType weaponType) override; + void ResetAllWeaponAimCameraOverrides() override; }; diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 19a4e92d911..32d42e17840 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -3492,6 +3492,7 @@ void CClientGame::Event_OnIngame() g_pGame->GetWaterManager()->Reset(); // Deletes all custom water elements, ResetMapInfo only reverts changes to water level g_pGame->GetWaterManager()->SetWaterDrawnLast(true); m_pCamera->ResetCameraClip(); + g_pGame->GetCamera()->ResetAllWeaponAimCameraOverrides(); // Deallocate all custom models m_pManager->GetModelManager()->RemoveAll(); diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaCameraDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaCameraDefs.cpp index 8a64fe4f997..bc8f3162770 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaCameraDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaCameraDefs.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #define MIN_CLIENT_REQ_SETCAMERATARGET_USE_ANY_ELEMENTS "1.5.8-9.20979" @@ -45,6 +46,14 @@ void CLuaCameraDefs::LoadFunctions() {"shakeCamera", ArgumentParser}, {"resetShakeCamera", ArgumentParser}, + + // Weapon aim camera overrides + {"setWeaponAimCameraOffset", ArgumentParserWarn}, + {"getWeaponAimCameraOffset", ArgumentParserWarn}, + {"resetWeaponAimCameraOffset", ArgumentParserWarn}, + {"setWeaponAimCameraZoom", ArgumentParserWarn}, + {"getWeaponAimCameraZoom", ArgumentParserWarn}, + {"resetWeaponAimCameraZoom", ArgumentParserWarn}, }; // Add functions @@ -576,3 +585,59 @@ bool CLuaCameraDefs::ResetShakeCamera() noexcept m_pManager->GetCamera()->ResetShakeCamera(); return true; } + +bool CLuaCameraDefs::SetWeaponAimCameraOffset(int weaponType, float fX, float fY, float fZ) +{ + if (weaponType <= WEAPONTYPE_UNARMED || weaponType >= WEAPONTYPE_LAST_WEAPONTYPE) + throw std::invalid_argument("Invalid weapon type"); + + g_pGame->GetCamera()->SetWeaponAimCameraOffset(static_cast(weaponType), fX, fY, fZ); + return true; +} + +CLuaMultiReturn CLuaCameraDefs::GetWeaponAimCameraOffset(int weaponType) +{ + if (weaponType <= WEAPONTYPE_UNARMED || weaponType >= WEAPONTYPE_LAST_WEAPONTYPE) + throw std::invalid_argument("Invalid weapon type"); + + float fX, fY, fZ; + g_pGame->GetCamera()->GetWeaponAimCameraOffset(static_cast(weaponType), fX, fY, fZ); + return {fX, fY, fZ}; +} + +bool CLuaCameraDefs::SetWeaponAimCameraZoom(int weaponType, float fFOV) +{ + if (weaponType <= WEAPONTYPE_UNARMED || weaponType >= WEAPONTYPE_LAST_WEAPONTYPE) + throw std::invalid_argument("Invalid weapon type"); + if (fFOV < 0.0f || fFOV > 179.0f) + throw std::invalid_argument("Invalid FOV range (0-179)"); + + g_pGame->GetCamera()->SetWeaponAimCameraZoom(static_cast(weaponType), fFOV); + return true; +} + +float CLuaCameraDefs::GetWeaponAimCameraZoom(int weaponType) +{ + if (weaponType <= WEAPONTYPE_UNARMED || weaponType >= WEAPONTYPE_LAST_WEAPONTYPE) + throw std::invalid_argument("Invalid weapon type"); + + return g_pGame->GetCamera()->GetWeaponAimCameraZoom(static_cast(weaponType)); +} + +bool CLuaCameraDefs::ResetWeaponAimCameraOffset(int weaponType) +{ + if (weaponType <= WEAPONTYPE_UNARMED || weaponType >= WEAPONTYPE_LAST_WEAPONTYPE) + throw std::invalid_argument("Invalid weapon type"); + + g_pGame->GetCamera()->ResetWeaponAimCameraOffset(static_cast(weaponType)); + return true; +} + +bool CLuaCameraDefs::ResetWeaponAimCameraZoom(int weaponType) +{ + if (weaponType <= WEAPONTYPE_UNARMED || weaponType >= WEAPONTYPE_LAST_WEAPONTYPE) + throw std::invalid_argument("Invalid weapon type"); + + g_pGame->GetCamera()->ResetWeaponAimCameraZoom(static_cast(weaponType)); + return true; +} diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaCameraDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaCameraDefs.h index b233d4e39c8..b9b4d21e981 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaCameraDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaCameraDefs.h @@ -47,6 +47,14 @@ class CLuaCameraDefs : public CLuaDefs static bool ShakeCamera(float radius, std::optional x, std::optional y, std::optional z) noexcept; static bool ResetShakeCamera() noexcept; + // Weapon aim camera overrides + static bool SetWeaponAimCameraOffset(int weaponType, float fX, float fY, float fZ); + static CLuaMultiReturn GetWeaponAimCameraOffset(int weaponType); + static bool ResetWeaponAimCameraOffset(int weaponType); + static bool SetWeaponAimCameraZoom(int weaponType, float fFOV); + static float GetWeaponAimCameraZoom(int weaponType); + static bool ResetWeaponAimCameraZoom(int weaponType); + // For OOP only LUA_DECLARE(OOP_GetCameraPosition); LUA_DECLARE(OOP_SetCameraPosition); diff --git a/Client/sdk/game/CCamera.h b/Client/sdk/game/CCamera.h index 079448e4de6..4c3036fb74a 100644 --- a/Client/sdk/game/CCamera.h +++ b/Client/sdk/game/CCamera.h @@ -12,6 +12,7 @@ #pragma once #include "CEntity.h" +#include "CWeaponInfo.h" class CMatrix; class CCam; @@ -157,4 +158,13 @@ class CCamera virtual bool GetTransitionMatrix(CMatrix& matrix) const = 0; virtual bool IsSphereVisible(CVector* center, float radius) const = 0; + + // Per-weapon aiming camera. Offset is in camera space (x=right, y=up, z=forward). + virtual void SetWeaponAimCameraOffset(eWeaponType weaponType, float fX, float fY, float fZ) = 0; + virtual void GetWeaponAimCameraOffset(eWeaponType weaponType, float& fX, float& fY, float& fZ) = 0; + virtual void ResetWeaponAimCameraOffset(eWeaponType weaponType) = 0; + virtual void SetWeaponAimCameraZoom(eWeaponType weaponType, float fFOV) = 0; + virtual float GetWeaponAimCameraZoom(eWeaponType weaponType) = 0; + virtual void ResetWeaponAimCameraZoom(eWeaponType weaponType) = 0; + virtual void ResetAllWeaponAimCameraOverrides() = 0; };