From 71d4d61ecf747c8a7c0c43cab3db29a14e567839 Mon Sep 17 00:00:00 2001 From: TheCrazy17 Date: Tue, 1 Sep 2026 09:49:27 -0300 Subject: [PATCH 1/4] Add createWaterCannon for script-controlled standalone water jets createWaterCannon/setWaterCannonDirection/setWaterCannonForce/ setWaterCannonEnabled/setWaterCannonModel (plus getters and the OOP WaterCannon class) let a script aim and spray a water jet anywhere, independent of any vehicle. A custom cannon runs on its own native CWaterCannon instance, kept entirely apart from the Firetruck/SWAT tank's own fixed-size array (CWaterCannons::aCannons); CMultiplayerSA_WaterCannons.cpp drives a separate list of these from the same two call sites that already run the vehicle-owned array every frame (CWaterCannons::Update/Render), without ever touching that array. A script spawning many custom cannons can this way never starve a real vehicle's own cannon of a slot, capped at 256 only as a memory/perf sanity limit of its own, not a native constraint. Vehicle water cannons are untouched by this. setWaterCannonModel optionally attaches a plain CClientObject kept aimed with the same heading/pitch-from-direction formula CAutomobile::FireTruckControl/TankControl already use for their own nozzle, since the native jet itself needs no visible part to work. --- Client/mods/deathmatch/StdInc.h | 2 + Client/mods/deathmatch/logic/CClientEntity.h | 2 + .../mods/deathmatch/logic/CClientManager.cpp | 5 + Client/mods/deathmatch/logic/CClientManager.h | 3 + .../deathmatch/logic/CClientWaterCannon.cpp | 70 +++++++++ .../deathmatch/logic/CClientWaterCannon.h | 75 +++++++++ .../logic/CClientWaterCannonManager.cpp | 69 ++++++++ .../logic/CClientWaterCannonManager.h | 44 ++++++ .../logic/CStaticFunctionDefinitions.cpp | 11 ++ .../logic/CStaticFunctionDefinitions.h | 3 + Client/mods/deathmatch/logic/lua/CLuaMain.cpp | 1 + .../mods/deathmatch/logic/lua/CLuaManager.cpp | 1 + .../logic/luadefs/CLuaWaterCannonDefs.cpp | 95 +++++++++++ .../logic/luadefs/CLuaWaterCannonDefs.h | 32 ++++ Client/multiplayer_sa/CMultiplayerSA.cpp | 1 + Client/multiplayer_sa/CMultiplayerSA.h | 4 + .../CMultiplayerSA_WaterCannons.cpp | 147 ++++++++++++++++++ Client/sdk/multiplayer/CMultiplayer.h | 7 + 18 files changed, 572 insertions(+) create mode 100644 Client/mods/deathmatch/logic/CClientWaterCannon.cpp create mode 100644 Client/mods/deathmatch/logic/CClientWaterCannon.h create mode 100644 Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp create mode 100644 Client/mods/deathmatch/logic/CClientWaterCannonManager.h create mode 100644 Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp create mode 100644 Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h create mode 100644 Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp diff --git a/Client/mods/deathmatch/StdInc.h b/Client/mods/deathmatch/StdInc.h index b676ad0f469..c9ea30d8e12 100644 --- a/Client/mods/deathmatch/StdInc.h +++ b/Client/mods/deathmatch/StdInc.h @@ -84,6 +84,7 @@ #include #include #include +#include #include #include #include @@ -143,6 +144,7 @@ #include #include #include +#include #include #include #include diff --git a/Client/mods/deathmatch/logic/CClientEntity.h b/Client/mods/deathmatch/logic/CClientEntity.h index 4ddd8baefb2..8c49b71947a 100644 --- a/Client/mods/deathmatch/logic/CClientEntity.h +++ b/Client/mods/deathmatch/logic/CClientEntity.h @@ -81,6 +81,7 @@ enum eClientEntityType CCLIENTUNKNOWN, CCLIENTIMG, CCLIENTBUILDING, + CCLIENTWATERCANNON, }; class CEntity; @@ -146,6 +147,7 @@ enum eCClientEntityClassTypes CLASS_CClientSearchLight, CLASS_CClientIMG, CLASS_CClientBuilding, + CLASS_CClientWaterCannon, }; class CClientEntity : public CClientEntityBase diff --git a/Client/mods/deathmatch/logic/CClientManager.cpp b/Client/mods/deathmatch/logic/CClientManager.cpp index bf79bf9ec93..78681d28930 100644 --- a/Client/mods/deathmatch/logic/CClientManager.cpp +++ b/Client/mods/deathmatch/logic/CClientManager.cpp @@ -55,6 +55,7 @@ CClientManager::CClientManager() m_pPacketRecorder = new CClientPacketRecorder(this); m_pImgManager = new CClientIMGManager(this); m_pBuildingManager = new CClientBuildingManager(this); + m_pWaterCannonManager = new CClientWaterCannonManager(this); m_bBeingDeleted = false; m_bGameUnloadedFlag = false; @@ -181,6 +182,9 @@ CClientManager::~CClientManager() delete m_pBuildingManager; m_pBuildingManager = nullptr; + + delete m_pWaterCannonManager; + m_pWaterCannonManager = nullptr; } // @@ -218,6 +222,7 @@ void CClientManager::DoPulse(bool bDoStandardPulses, bool bDoVehicleManagerPulse m_pColManager->DoPulse(); m_pGUIManager->DoPulse(); m_pWeaponManager->DoPulse(); + m_pWaterCannonManager->DoPulse(); } else { diff --git a/Client/mods/deathmatch/logic/CClientManager.h b/Client/mods/deathmatch/logic/CClientManager.h index 26f93f0015b..ce372662ecf 100644 --- a/Client/mods/deathmatch/logic/CClientManager.h +++ b/Client/mods/deathmatch/logic/CClientManager.h @@ -44,6 +44,7 @@ class CClientManager; #include "CClientModelManager.h" #include "CClientIMGManager.h" #include "CClientBuildingManager.h" +#include "CClientWaterCannonManager.h" class CClientProjectileManager; class CClientExplosionManager; @@ -96,6 +97,7 @@ class CClientManager CClientWeaponManager* GetWeaponManager() { return m_pWeaponManager; } CClientEffectManager* GetEffectManager() { return m_pEffectManager; } CClientPointLightsManager* GetPointLightsManager() { return m_pPointLightsManager; } + CClientWaterCannonManager* GetWaterCannonManager() { return m_pWaterCannonManager; } CClientIMGManager* GetIMGManager() { return m_pImgManager; } CClientBuildingManager* GetBuildingManager() const noexcept { return m_pBuildingManager; } @@ -150,6 +152,7 @@ class CClientManager CClientWeaponManager* m_pWeaponManager; CClientEffectManager* m_pEffectManager; CClientPointLightsManager* m_pPointLightsManager; + CClientWaterCannonManager* m_pWaterCannonManager; CClientModelManager* m_pModelManager; CClientIMGManager* m_pImgManager; CClientPacketRecorder* m_pPacketRecorder; diff --git a/Client/mods/deathmatch/logic/CClientWaterCannon.cpp b/Client/mods/deathmatch/logic/CClientWaterCannon.cpp new file mode 100644 index 00000000000..605c025a1d8 --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWaterCannon.cpp @@ -0,0 +1,70 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * (Shared logic for modifications) + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/CClientWaterCannon.cpp + * PURPOSE: Water cannon entity class + * + *****************************************************************************/ + +#include + +CClientWaterCannon::CClientWaterCannon(CClientManager* pManager, ElementID ID) : ClassInit(this), CClientEntity(ID) +{ + m_pWaterCannonManager = pManager->GetWaterCannonManager(); + + m_vecDirection = CVector(0.0f, 1.0f, 0.0f); + m_fForce = 1.0f; + m_bEnabled = true; + m_pNativeCannon = g_pMultiplayer->CreateCustomWaterCannon(); + + SetTypeName("water-cannon"); + + m_pWaterCannonManager->AddToList(this); +} + +CClientWaterCannon::~CClientWaterCannon() +{ + Unlink(); + g_pMultiplayer->DestroyCustomWaterCannon(m_pNativeCannon); +} + +void CClientWaterCannon::Unlink() +{ + m_pWaterCannonManager->RemoveFromList(this); +} + +void CClientWaterCannon::DoPulse() +{ + // Update our position/rotation if we're attached, and let anything attached to us follow ours + DoAttaching(); + + if (!m_bEnabled || !m_pNativeCannon) + return; + + // m_vecDirection is always normalized (see SetDirection), so this is purely "orientation + // times force", not the direction's own magnitude bleeding into the velocity. + const CVector vecVelocity = m_vecDirection * m_fForce; + g_pMultiplayer->UpdateCustomWaterCannon(m_pNativeCannon, m_vecPosition, vecVelocity); +} + +// Same heading/pitch-from-direction convention the native water cannons use +// (CAutomobile::FireTruckControl/TankControl), so a model attached to us via attachElements +// aims consistently with how the game's own nozzles look. atan2f's second argument here is +// never negative, so pitch always comes back between -90 and 90 degrees for a normalized +// direction, which is exactly the range SetRotationRadians below expects back. +void CClientWaterCannon::GetRotationRadians(CVector& vecOutRadians) const +{ + const float fHorizontalMagnitude = sqrtf(m_vecDirection.fX * m_vecDirection.fX + m_vecDirection.fY * m_vecDirection.fY); + const float fHeading = atan2f(-m_vecDirection.fX, m_vecDirection.fY); + const float fPitch = atan2f(m_vecDirection.fZ, fHorizontalMagnitude); + vecOutRadians = CVector(fPitch, 0.0f, fHeading); +} + +void CClientWaterCannon::SetRotationRadians(const CVector& vecRadians) +{ + const float fPitch = vecRadians.fX; + const float fHeading = vecRadians.fZ; + SetDirection(CVector(-sinf(fHeading) * cosf(fPitch), cosf(fHeading) * cosf(fPitch), sinf(fPitch))); +} diff --git a/Client/mods/deathmatch/logic/CClientWaterCannon.h b/Client/mods/deathmatch/logic/CClientWaterCannon.h new file mode 100644 index 00000000000..552e64679d7 --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWaterCannon.h @@ -0,0 +1,75 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * (Shared logic for modifications) + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/CClientWaterCannon.h + * PURPOSE: Water cannon entity class header + * + *****************************************************************************/ + +#pragma once + +#include "CClientEntity.h" + +class CClientWaterCannonManager; + +// A standalone water jet using the native CWaterCannons system (the same one the Firetruck and +// SWAT tank use), but not tied to any real vehicle. See CClientWaterCannon.cpp for how the native +// side treats the "owner" id as fully opaque, which is what makes that safe. +class CClientWaterCannon final : public CClientEntity +{ + DECLARE_CLASS(CClientWaterCannon, CClientEntity) + friend class CClientWaterCannonManager; + +public: + CClientWaterCannon(CClientManager* pManager, ElementID ID); + ~CClientWaterCannon(); + + void Unlink(); + + eClientEntityType GetType() const { return CCLIENTWATERCANNON; }; + + void GetPosition(CVector& vecPosition) const { vecPosition = m_vecPosition; }; + void SetPosition(const CVector& vecPosition) { m_vecPosition = vecPosition; }; + + // The generic element rotation, kept in sync with the direction below so that scripts can + // attach a model to us with the standard attachElements and have it aim correctly; see + // CClientWaterCannon.cpp for the angle convention shared with the direction accessors. + void GetRotationRadians(CVector& vecOutRadians) const; + void SetRotationRadians(const CVector& vecRadians); + + // Always stored normalized, so it's a pure orientation; SetForce's value is the only thing + // that controls the velocity's actual magnitude. + const CVector& GetDirection() const { return m_vecDirection; }; + void SetDirection(CVector vecDirection) + { + vecDirection.Normalize(); + m_vecDirection = vecDirection; + }; + + float GetForce() const { return m_fForce; }; + void SetForce(float fForce) { m_fForce = fForce; }; + + bool IsEnabled() const { return m_bEnabled; }; + void SetEnabled(bool bEnabled) { m_bEnabled = bEnabled; }; + + // False if the shared custom-cannon pool (CMultiplayer::CreateCustomWaterCannon, capped + // separately from the vehicle-owned one) was full at construction time. + bool HasNativeCannon() const { return m_pNativeCannon != nullptr; }; + +protected: + void DoPulse(); + +private: + CClientWaterCannonManager* m_pWaterCannonManager; + + CVector m_vecPosition; + CVector m_vecDirection; + float m_fForce; + bool m_bEnabled; + + // Opaque handle from CMultiplayer::CreateCustomWaterCannon; our own native CWaterCannon + // instance, kept apart from the vehicle-owned ones (see CClientWaterCannon.cpp). + void* m_pNativeCannon; +}; diff --git a/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp b/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp new file mode 100644 index 00000000000..d184fba0551 --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp @@ -0,0 +1,69 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * (Shared logic for modifications) + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/CClientWaterCannonManager.cpp + * PURPOSE: Water cannon entity manager class + * + *****************************************************************************/ + +#include + +CClientWaterCannonManager::CClientWaterCannonManager(CClientManager* pManager) +{ + m_pManager = pManager; + m_bDontRemoveFromList = false; +} + +CClientWaterCannonManager::~CClientWaterCannonManager() +{ + DeleteAll(); +} + +CClientWaterCannon* CClientWaterCannonManager::Create(ElementID ID) +{ + if (Get(ID)) + return nullptr; + + CClientWaterCannon* pCannon = new CClientWaterCannon(m_pManager, ID); + if (!pCannon->HasNativeCannon()) + { + // The custom-cannon pool (separate from the vehicle-owned one, but still capped) is full + delete pCannon; + return nullptr; + } + + return pCannon; +} + +void CClientWaterCannonManager::DeleteAll() +{ + m_bDontRemoveFromList = true; + for (CClientWaterCannon* pCannon : m_List) + delete pCannon; + m_bDontRemoveFromList = false; + + m_List.clear(); +} + +CClientWaterCannon* CClientWaterCannonManager::Get(ElementID ID) +{ + CClientEntity* pEntity = CElementIDs::GetElement(ID); + if (pEntity && pEntity->GetType() == CCLIENTWATERCANNON) + return static_cast(pEntity); + + return nullptr; +} + +void CClientWaterCannonManager::DoPulse() +{ + for (CClientWaterCannon* pCannon : m_List) + pCannon->DoPulse(); +} + +void CClientWaterCannonManager::RemoveFromList(CClientWaterCannon* pCannon) +{ + if (!m_bDontRemoveFromList && !m_List.empty()) + m_List.remove(pCannon); +} diff --git a/Client/mods/deathmatch/logic/CClientWaterCannonManager.h b/Client/mods/deathmatch/logic/CClientWaterCannonManager.h new file mode 100644 index 00000000000..3fad714fe98 --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWaterCannonManager.h @@ -0,0 +1,44 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * (Shared logic for modifications) + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/CClientWaterCannonManager.h + * PURPOSE: Water cannon entity manager class header + * + *****************************************************************************/ + +#pragma once + +#include "CClientWaterCannon.h" +#include + +class CClientWaterCannonManager +{ + friend class CClientManager; + friend class CClientWaterCannon; + +public: + CClientWaterCannonManager(CClientManager* pManager); + ~CClientWaterCannonManager(); + + CClientWaterCannon* Create(ElementID ID); + + void DeleteAll(); + + std::list::const_iterator IterBegin() { return m_List.begin(); }; + std::list::const_iterator IterEnd() { return m_List.end(); }; + + static CClientWaterCannon* Get(ElementID ID); + + void DoPulse(); + +private: + void AddToList(CClientWaterCannon* pCannon) { m_List.push_back(pCannon); }; + void RemoveFromList(CClientWaterCannon* pCannon); + + CClientManager* m_pManager; + + std::list m_List; + bool m_bDontRemoveFromList; +}; diff --git a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp index 9cefd79ce91..8bc5b556a89 100644 --- a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp +++ b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp @@ -7758,6 +7758,17 @@ CClientWeapon* CStaticFunctionDefinitions::CreateWeapon(CResource& Resource, eWe return pWeapon; } +CClientWaterCannon* CStaticFunctionDefinitions::CreateWaterCannon(CResource& Resource, const CVector& vecPosition) +{ + CClientWaterCannon* pCannon = m_pManager->GetWaterCannonManager()->Create(INVALID_ELEMENT_ID); + if (!pCannon) + return nullptr; + + pCannon->SetPosition(vecPosition); + pCannon->SetParent(Resource.GetResourceDynamicEntity()); + return pCannon; +} + bool CStaticFunctionDefinitions::FireWeapon(CClientWeapon* pWeapon) { if (pWeapon) diff --git a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.h b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.h index d1e019758e9..75a1d6c796f 100644 --- a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.h +++ b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.h @@ -717,6 +717,9 @@ class CStaticFunctionDefinitions static bool GetWeaponFlags(CClientWeapon* pWeapon, SLineOfSightFlags& flags); static bool GetWeaponFlags(CClientWeapon* pWeapon, eWeaponFlags flag, bool& bData); + // Water cannon funcs + static CClientWaterCannon* CreateWaterCannon(CResource& Resource, const CVector& vecPosition); + static bool SetWeaponFiringRate(CClientWeapon* pWeapon, int iFiringRate); static bool ResetWeaponFiringRate(CClientWeapon* pWeapon); static bool GetWeaponFiringRate(CClientWeapon* pWeapon, int& iFiringRate); diff --git a/Client/mods/deathmatch/logic/lua/CLuaMain.cpp b/Client/mods/deathmatch/logic/lua/CLuaMain.cpp index 91f26561267..6f0d4d6e1c6 100644 --- a/Client/mods/deathmatch/logic/lua/CLuaMain.cpp +++ b/Client/mods/deathmatch/logic/lua/CLuaMain.cpp @@ -127,6 +127,7 @@ void CLuaMain::InitClasses(lua_State* luaVM) CLuaVehicleDefs::AddClass(luaVM); CLuaWaterDefs::AddClass(luaVM); CLuaWeaponDefs::AddClass(luaVM); + CLuaWaterCannonDefs::AddClass(luaVM); CLuaBuildingDefs::AddClass(luaVM); CLuaShared::AddClasses(luaVM); diff --git a/Client/mods/deathmatch/logic/lua/CLuaManager.cpp b/Client/mods/deathmatch/logic/lua/CLuaManager.cpp index e97f802e612..01c7ba661b5 100644 --- a/Client/mods/deathmatch/logic/lua/CLuaManager.cpp +++ b/Client/mods/deathmatch/logic/lua/CLuaManager.cpp @@ -279,6 +279,7 @@ void CLuaManager::LoadCFunctions() CLuaVehicleDefs::LoadFunctions(); CLuaWaterDefs::LoadFunctions(); CLuaWeaponDefs::LoadFunctions(); + CLuaWaterCannonDefs::LoadFunctions(); CLuaWorldDefs::LoadFunctions(); CLuaXMLDefs::LoadFunctions(); CLuaClientDefs::LoadFunctions(); diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp new file mode 100644 index 00000000000..7e61134f711 --- /dev/null +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp @@ -0,0 +1,95 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/luadefs/CLuaWaterCannonDefs.cpp + * PURPOSE: Lua definitions class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#include "StdInc.h" + +void CLuaWaterCannonDefs::LoadFunctions() +{ + constexpr static const std::pair functions[]{ + {"createWaterCannon", ArgumentParser}, + {"setWaterCannonDirection", ArgumentParser}, + {"getWaterCannonDirection", ArgumentParser}, + {"setWaterCannonForce", ArgumentParser}, + {"getWaterCannonForce", ArgumentParser}, + {"setWaterCannonEnabled", ArgumentParser}, + {"isWaterCannonEnabled", ArgumentParser}, + }; + + for (const auto& [name, func] : functions) + CLuaCFunctions::AddFunction(name, func); +} + +void CLuaWaterCannonDefs::AddClass(lua_State* luaVM) +{ + lua_newclass(luaVM); + + lua_classfunction(luaVM, "create", "createWaterCannon"); + + lua_classfunction(luaVM, "setDirection", "setWaterCannonDirection"); + lua_classfunction(luaVM, "setForce", "setWaterCannonForce"); + lua_classfunction(luaVM, "setEnabled", "setWaterCannonEnabled"); + + lua_classfunction(luaVM, "getDirection", "getWaterCannonDirection"); + lua_classfunction(luaVM, "getForce", "getWaterCannonForce"); + lua_classfunction(luaVM, "isEnabled", "isWaterCannonEnabled"); + + lua_classvariable(luaVM, "direction", "setWaterCannonDirection", "getWaterCannonDirection"); + lua_classvariable(luaVM, "force", "setWaterCannonForce", "getWaterCannonForce"); + lua_classvariable(luaVM, "enabled", "setWaterCannonEnabled", "isWaterCannonEnabled"); + + lua_registerclass(luaVM, "WaterCannon", "Element"); +} + +std::variant CLuaWaterCannonDefs::CreateWaterCannon(lua_State* luaVM, CVector vecPosition) +{ + CResource& resource = lua_getownerresource(luaVM); + CClientWaterCannon* pCannon = CStaticFunctionDefinitions::CreateWaterCannon(resource, vecPosition); + if (!pCannon) + return false; + + if (CElementGroup* elementGroup = resource.GetElementGroup()) + elementGroup->Add(pCannon); + + return pCannon; +} + +bool CLuaWaterCannonDefs::SetWaterCannonDirection(CClientWaterCannon* pCannon, CVector vecDirection) +{ + pCannon->SetDirection(vecDirection); + return true; +} + +CVector CLuaWaterCannonDefs::GetWaterCannonDirection(CClientWaterCannon* pCannon) +{ + return pCannon->GetDirection(); +} + +bool CLuaWaterCannonDefs::SetWaterCannonForce(CClientWaterCannon* pCannon, float fForce) +{ + pCannon->SetForce(fForce); + return true; +} + +float CLuaWaterCannonDefs::GetWaterCannonForce(CClientWaterCannon* pCannon) +{ + return pCannon->GetForce(); +} + +bool CLuaWaterCannonDefs::SetWaterCannonEnabled(CClientWaterCannon* pCannon, bool bEnabled) +{ + pCannon->SetEnabled(bEnabled); + return true; +} + +bool CLuaWaterCannonDefs::IsWaterCannonEnabled(CClientWaterCannon* pCannon) +{ + return pCannon->IsEnabled(); +} diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h new file mode 100644 index 00000000000..9703772a91a --- /dev/null +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h @@ -0,0 +1,32 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/luadefs/CLuaWaterCannonDefs.h + * PURPOSE: Lua definitions class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once +#include "CLuaDefs.h" +#include + +class CLuaWaterCannonDefs : public CLuaDefs +{ +public: + static void LoadFunctions(); + static void AddClass(lua_State* luaVM); + + static std::variant CreateWaterCannon(lua_State* luaVM, CVector vecPosition); + + static bool SetWaterCannonDirection(CClientWaterCannon* pCannon, CVector vecDirection); + static CVector GetWaterCannonDirection(CClientWaterCannon* pCannon); + + static bool SetWaterCannonForce(CClientWaterCannon* pCannon, float fForce); + static float GetWaterCannonForce(CClientWaterCannon* pCannon); + + static bool SetWaterCannonEnabled(CClientWaterCannon* pCannon, bool bEnabled); + static bool IsWaterCannonEnabled(CClientWaterCannon* pCannon); +}; diff --git a/Client/multiplayer_sa/CMultiplayerSA.cpp b/Client/multiplayer_sa/CMultiplayerSA.cpp index af42702f3f8..80defd9a273 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA.cpp @@ -1586,6 +1586,7 @@ void CMultiplayerSA::InitHooks() InitHooks_VehicleDamage(); InitHooks_VehicleLights(); InitHooks_VehicleWeapons(); + InitHooks_WaterCannons(); InitHooks_Streaming(); InitHooks_FrameRateFixes(); diff --git a/Client/multiplayer_sa/CMultiplayerSA.h b/Client/multiplayer_sa/CMultiplayerSA.h index 9f85b307909..a6464ac66b4 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.h +++ b/Client/multiplayer_sa/CMultiplayerSA.h @@ -77,6 +77,7 @@ class CMultiplayerSA : public CMultiplayer void InitHooks_VehicleLights(); void InitHooks_VehicleDamage(); void InitHooks_VehicleWeapons(); + void InitHooks_WaterCannons(); void InitHooks_Direct3D(); void InitHooks_FixLineOfSightArgs(); void InitHooks_Streaming(); @@ -140,6 +141,9 @@ class CMultiplayerSA : public CMultiplayer void SetObjectDamageHandler(ObjectDamageHandler* pHandler); void SetObjectBreakHandler(ObjectBreakHandler* pHandler); void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler); + void* CreateCustomWaterCannon(); + void DestroyCustomWaterCannon(void* pCannon); + void UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity); void SetVehicleFellThroughMapHandler(VehicleFellThroughMapHandler* pHandler); void SetGameObjectDestructHandler(GameObjectDestructHandler* pHandler); void SetGameVehicleDestructHandler(GameVehicleDestructHandler* pHandler); diff --git a/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp b/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp new file mode 100644 index 00000000000..5c4b383dd63 --- /dev/null +++ b/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp @@ -0,0 +1,147 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: multiplayer_sa/CMultiplayerSA_WaterCannons.cpp + * PURPOSE: Script-owned water cannons, kept apart from the vehicle-owned ones + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ +#include "StdInc.h" +#include +#include + +// Native CWaterCannon (per-instance) methods. The Firetruck/SWAT tank's own water cannons live in +// a single fixed-size, byte-limited array (CWaterCannons::aCannons, see CGameSA.h) that every +// vehicle shares; a script spawning many custom cannons into that same array could starve a real +// vehicle of a slot. Calling these directly on our own, separately allocated instances instead +// keeps the two pools from ever competing for space. +#define FUNC_CWaterCannon_Constructor 0x728B10 +#define FUNC_CWaterCannon_Destructor 0x728B30 +#define FUNC_CWaterCannon_Init 0x728B40 +#define FUNC_CWaterCannon_UpdateNewInput 0x728C20 +#define FUNC_CWaterCannon_UpdateOncePerFrame 0x72A280 +#define FUNC_CWaterCannon_Render 0x728DA0 +#define SIZE_CWaterCannon 0x3CC + +// Not a native constraint, since these live entirely outside the vehicles' own fixed-size array; +// just a sanity ceiling so a runaway script can't grow this list without bound. Update_OncePerFrame +// runs a world-wide ped scan for every entry every few frames, so this is also a soft cost limit. +static constexpr size_t MAX_CUSTOM_WATER_CANNONS = 256; + +static std::vector customWaterCannons; + +void* CMultiplayerSA::CreateCustomWaterCannon() +{ + if (customWaterCannons.size() >= MAX_CUSTOM_WATER_CANNONS) + return nullptr; + + void* pCannon = malloc(SIZE_CWaterCannon); + memset(pCannon, 0, SIZE_CWaterCannon); + ((void(__thiscall*)(void*))FUNC_CWaterCannon_Constructor)(pCannon); + ((void(__thiscall*)(void*))FUNC_CWaterCannon_Init)(pCannon); + + customWaterCannons.push_back(pCannon); + return pCannon; +} + +void CMultiplayerSA::DestroyCustomWaterCannon(void* pCannon) +{ + if (!pCannon) + return; + + const auto iter = std::find(customWaterCannons.begin(), customWaterCannons.end(), pCannon); + if (iter == customWaterCannons.end()) + return; + + customWaterCannons.erase(iter); + + ((void(__thiscall*)(void*))FUNC_CWaterCannon_Destructor)(pCannon); + free(pCannon); +} + +void CMultiplayerSA::UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity) +{ + if (!pCannon) + return; + + ((void(__thiscall*)(void*, const CVector*, const CVector*))FUNC_CWaterCannon_UpdateNewInput)(pCannon, &vecStart, &vecVelocity); +} + +static void UpdateAllCustomWaterCannons() +{ + std::int16_t index = 0; + for (void* pCannon : customWaterCannons) + ((void(__thiscall*)(void*, std::int16_t))FUNC_CWaterCannon_UpdateOncePerFrame)(pCannon, index++); +} + +static void RenderAllCustomWaterCannons() +{ + for (void* pCannon : customWaterCannons) + ((void(__thiscall*)(void*))FUNC_CWaterCannon_Render)(pCannon); +} + +////////////////////////////////////////////////////////////////////////////////////////// +// The single call site for CWaterCannons::Update, so our own custom cannons age, animate and +// extinguish fires alongside the vehicle-owned ones every frame, without touching their array. +////////////////////////////////////////////////////////////////////////////////////////// +// >>> 0x53C086 | E8 35 63 1E 00 | call 0x0072A3C0 +// 0x53C08B | E8 10 70 03 00 | call 0x005720A0 +static const DWORD FUNC_CWaterCannons_Update = 0x0072A3C0; +#define HOOKPOS_CWaterCannons_UpdateCustomDispatch 0x53C086 +#define HOOKSIZE_CWaterCannons_UpdateCustomDispatch 5 +static const DWORD CONTINUE_CWaterCannons_UpdateCustomDispatch = 0x53C08B; + +static void __declspec(naked) HOOK_CWaterCannons_UpdateCustomDispatch() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + // this call sits in a flat chain of independent, argument-less subsystem update calls; + // none of them read a register left behind by the previous one, so nothing needs saving + call UpdateAllCustomWaterCannons + + call FUNC_CWaterCannons_Update + jmp CONTINUE_CWaterCannons_UpdateCustomDispatch + } + // clang-format on +} + +////////////////////////////////////////////////////////////////////////////////////////// +// The single call site for CWaterCannons::Render, same reasoning as the Update dispatch above. +////////////////////////////////////////////////////////////////////////////////////////// +// >>> 0x53E1A5 | E8 86 D7 1F 00 | call 0x00729B30 +// 0x53E1AA | E8 B1 95 12 00 | call 0x006E7760 +static const DWORD FUNC_CWaterCannons_Render = 0x00729B30; +#define HOOKPOS_CWaterCannons_RenderCustomDispatch 0x53E1A5 +#define HOOKSIZE_CWaterCannons_RenderCustomDispatch 5 +static const DWORD CONTINUE_CWaterCannons_RenderCustomDispatch = 0x53E1AA; + +static void __declspec(naked) HOOK_CWaterCannons_RenderCustomDispatch() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + call RenderAllCustomWaterCannons + + call FUNC_CWaterCannons_Render + jmp CONTINUE_CWaterCannons_RenderCustomDispatch + } + // clang-format on +} + +////////////////////////////////////////////////////////////////////////////////////////// +// +// CMultiplayerSA::InitHooks_WaterCannons +// +////////////////////////////////////////////////////////////////////////////////////////// +void CMultiplayerSA::InitHooks_WaterCannons() +{ + EZHookInstall(CWaterCannons_UpdateCustomDispatch); + EZHookInstall(CWaterCannons_RenderCustomDispatch); +} diff --git a/Client/sdk/multiplayer/CMultiplayer.h b/Client/sdk/multiplayer/CMultiplayer.h index 28d2e074088..0023ffc3771 100644 --- a/Client/sdk/multiplayer/CMultiplayer.h +++ b/Client/sdk/multiplayer/CMultiplayer.h @@ -250,6 +250,13 @@ class CMultiplayer virtual void SetObjectDamageHandler(ObjectDamageHandler* pHandler) = 0; virtual void SetObjectBreakHandler(ObjectBreakHandler* pHandler) = 0; virtual void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler) = 0; + + // A script-owned water cannon, driven by its own list of native CWaterCannon instances kept + // entirely apart from the vehicle-owned ones (the Firetruck/SWAT tank's own aCannons array), + // so scripts spawning a lot of them can never starve a real vehicle's water cannon of a slot. + virtual void* CreateCustomWaterCannon() = 0; + virtual void DestroyCustomWaterCannon(void* pCannon) = 0; + virtual void UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity) = 0; virtual void SetVehicleFellThroughMapHandler(VehicleFellThroughMapHandler* pHandler) = 0; virtual void SetGameObjectDestructHandler(GameObjectDestructHandler* pHandler) = 0; virtual void SetGameVehicleDestructHandler(GameVehicleDestructHandler* pHandler) = 0; From 34a9eeb677009cd5c4cc603a5a3b19bda71bb89d Mon Sep 17 00:00:00 2001 From: TheCrazy17 Date: Tue, 1 Sep 2026 21:41:27 -0300 Subject: [PATCH 2/4] Give custom water cannons color and sound --- .../deathmatch/logic/CClientWaterCannon.cpp | 18 +++ .../deathmatch/logic/CClientWaterCannon.h | 9 ++ .../logic/luadefs/CLuaWaterCannonDefs.cpp | 32 ++++- .../logic/luadefs/CLuaWaterCannonDefs.h | 7 +- Client/multiplayer_sa/CMultiplayerSA.h | 98 +++++++-------- .../CMultiplayerSA_WaterCannons.cpp | 116 +++++++++++++++++- Client/sdk/multiplayer/CMultiplayer.h | 66 +++++----- 7 files changed, 262 insertions(+), 84 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientWaterCannon.cpp b/Client/mods/deathmatch/logic/CClientWaterCannon.cpp index 605c025a1d8..77f6da34214 100644 --- a/Client/mods/deathmatch/logic/CClientWaterCannon.cpp +++ b/Client/mods/deathmatch/logic/CClientWaterCannon.cpp @@ -17,6 +17,8 @@ CClientWaterCannon::CClientWaterCannon(CClientManager* pManager, ElementID ID) : m_vecDirection = CVector(0.0f, 1.0f, 0.0f); m_fForce = 1.0f; m_bEnabled = true; + m_bHasCustomColor = false; + m_Color = SColor(0xFFC8C8FF); // the native jet's light blue (R200 G200 B255), opaque m_pNativeCannon = g_pMultiplayer->CreateCustomWaterCannon(); SetTypeName("water-cannon"); @@ -35,6 +37,22 @@ void CClientWaterCannon::Unlink() m_pWaterCannonManager->RemoveFromList(this); } +void CClientWaterCannon::SetColor(const SColor& color) +{ + m_Color = color; + m_bHasCustomColor = true; + if (m_pNativeCannon) + g_pMultiplayer->SetCustomWaterCannonColor(m_pNativeCannon, color.R, color.G, color.B, color.A); +} + +void CClientWaterCannon::ResetColor() +{ + m_bHasCustomColor = false; + m_Color = SColor(0xFFC8C8FF); + if (m_pNativeCannon) + g_pMultiplayer->ResetCustomWaterCannonColor(m_pNativeCannon); +} + void CClientWaterCannon::DoPulse() { // Update our position/rotation if we're attached, and let anything attached to us follow ours diff --git a/Client/mods/deathmatch/logic/CClientWaterCannon.h b/Client/mods/deathmatch/logic/CClientWaterCannon.h index 552e64679d7..7def9b9804e 100644 --- a/Client/mods/deathmatch/logic/CClientWaterCannon.h +++ b/Client/mods/deathmatch/logic/CClientWaterCannon.h @@ -54,6 +54,12 @@ class CClientWaterCannon final : public CClientEntity bool IsEnabled() const { return m_bEnabled; }; void SetEnabled(bool bEnabled) { m_bEnabled = bEnabled; }; + // A custom jet colour; when unset the native fading light-blue is used + bool HasCustomColor() const { return m_bHasCustomColor; }; + const SColor& GetColor() const { return m_Color; }; + void SetColor(const SColor& color); + void ResetColor(); + // False if the shared custom-cannon pool (CMultiplayer::CreateCustomWaterCannon, capped // separately from the vehicle-owned one) was full at construction time. bool HasNativeCannon() const { return m_pNativeCannon != nullptr; }; @@ -69,6 +75,9 @@ class CClientWaterCannon final : public CClientEntity float m_fForce; bool m_bEnabled; + bool m_bHasCustomColor; + SColor m_Color; + // Opaque handle from CMultiplayer::CreateCustomWaterCannon; our own native CWaterCannon // instance, kept apart from the vehicle-owned ones (see CClientWaterCannon.cpp). void* m_pNativeCannon; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp index 7e61134f711..2c24e3fb924 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp @@ -21,6 +21,9 @@ void CLuaWaterCannonDefs::LoadFunctions() {"getWaterCannonForce", ArgumentParser}, {"setWaterCannonEnabled", ArgumentParser}, {"isWaterCannonEnabled", ArgumentParser}, + {"setWaterCannonColor", ArgumentParser}, + {"getWaterCannonColor", ArgumentParser}, + {"resetWaterCannonColor", ArgumentParser}, }; for (const auto& [name, func] : functions) @@ -36,10 +39,13 @@ void CLuaWaterCannonDefs::AddClass(lua_State* luaVM) lua_classfunction(luaVM, "setDirection", "setWaterCannonDirection"); lua_classfunction(luaVM, "setForce", "setWaterCannonForce"); lua_classfunction(luaVM, "setEnabled", "setWaterCannonEnabled"); + lua_classfunction(luaVM, "setColor", "setWaterCannonColor"); + lua_classfunction(luaVM, "resetColor", "resetWaterCannonColor"); lua_classfunction(luaVM, "getDirection", "getWaterCannonDirection"); lua_classfunction(luaVM, "getForce", "getWaterCannonForce"); lua_classfunction(luaVM, "isEnabled", "isWaterCannonEnabled"); + lua_classfunction(luaVM, "getColor", "getWaterCannonColor"); lua_classvariable(luaVM, "direction", "setWaterCannonDirection", "getWaterCannonDirection"); lua_classvariable(luaVM, "force", "setWaterCannonForce", "getWaterCannonForce"); @@ -48,13 +54,16 @@ void CLuaWaterCannonDefs::AddClass(lua_State* luaVM) lua_registerclass(luaVM, "WaterCannon", "Element"); } -std::variant CLuaWaterCannonDefs::CreateWaterCannon(lua_State* luaVM, CVector vecPosition) +std::variant CLuaWaterCannonDefs::CreateWaterCannon(lua_State* luaVM, CVector vecPosition, std::optional color) { - CResource& resource = lua_getownerresource(luaVM); + CResource& resource = lua_getownerresource(luaVM); CClientWaterCannon* pCannon = CStaticFunctionDefinitions::CreateWaterCannon(resource, vecPosition); if (!pCannon) return false; + if (color.has_value()) + pCannon->SetColor(color.value()); + if (CElementGroup* elementGroup = resource.GetElementGroup()) elementGroup->Add(pCannon); @@ -93,3 +102,22 @@ bool CLuaWaterCannonDefs::IsWaterCannonEnabled(CClientWaterCannon* pCannon) { return pCannon->IsEnabled(); } + +bool CLuaWaterCannonDefs::SetWaterCannonColor(CClientWaterCannon* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, + std::optional ucAlpha) +{ + pCannon->SetColor(SColorRGBA(ucRed, ucGreen, ucBlue, ucAlpha.value_or(255))); + return true; +} + +CLuaMultiReturn CLuaWaterCannonDefs::GetWaterCannonColor(CClientWaterCannon* pCannon) +{ + const SColor color = pCannon->GetColor(); + return {color.R, color.G, color.B, color.A}; +} + +bool CLuaWaterCannonDefs::ResetWaterCannonColor(CClientWaterCannon* pCannon) +{ + pCannon->ResetColor(); + return true; +} diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h index 9703772a91a..e2df88fe0eb 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h @@ -19,7 +19,7 @@ class CLuaWaterCannonDefs : public CLuaDefs static void LoadFunctions(); static void AddClass(lua_State* luaVM); - static std::variant CreateWaterCannon(lua_State* luaVM, CVector vecPosition); + static std::variant CreateWaterCannon(lua_State* luaVM, CVector vecPosition, std::optional color); static bool SetWaterCannonDirection(CClientWaterCannon* pCannon, CVector vecDirection); static CVector GetWaterCannonDirection(CClientWaterCannon* pCannon); @@ -29,4 +29,9 @@ class CLuaWaterCannonDefs : public CLuaDefs static bool SetWaterCannonEnabled(CClientWaterCannon* pCannon, bool bEnabled); static bool IsWaterCannonEnabled(CClientWaterCannon* pCannon); + + static bool SetWaterCannonColor(CClientWaterCannon* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, + std::optional ucAlpha); + static CLuaMultiReturn GetWaterCannonColor(CClientWaterCannon* pCannon); + static bool ResetWaterCannonColor(CClientWaterCannon* pCannon); }; diff --git a/Client/multiplayer_sa/CMultiplayerSA.h b/Client/multiplayer_sa/CMultiplayerSA.h index a6464ac66b4..67a581bfbf0 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.h +++ b/Client/multiplayer_sa/CMultiplayerSA.h @@ -109,54 +109,56 @@ class CMultiplayerSA : public CMultiplayer void DisableCloseRangeDamage(bool bDisable); void DisableBadDrivebyHitboxes(bool bDisable) { m_bBadDrivebyHitboxesDisabled = bDisable; } - bool GetExplosionsDisabled(); - void DisableExplosions(bool bDisabled); - void SetExplosionHandler(ExplosionHandler* pExplosionHandler); - void SetDamageHandler(DamageHandler* pDamageHandler); - void SetDeathHandler(DeathHandler* pDeathHandler); - void SetProjectileHandler(ProjectileHandler* pProjectileHandler); - void SetProjectileStopHandler(ProjectileStopHandler* pProjectileHandler); - void SetFireHandler(FireHandler* pFireHandler); - void SetBreakTowLinkHandler(BreakTowLinkHandler* pBreakTowLinkHandler); - void SetProcessCamHandler(ProcessCamHandler* pProcessCamHandler); - void SetChokingHandler(ChokingHandler* pChokingHandler); - void SetPreWeatherUpdateHandler(PreWeatherUpdateHandler* pHandler); - void SetPreWorldProcessHandler(PreWorldProcessHandler* pHandler); - void SetPostWorldProcessHandler(PostWorldProcessHandler* pHandler); - void SetPostWorldProcessPedsAfterPreRenderHandler(PostWorldProcessPedsAfterPreRenderHandler* pHandler); - void SetIdleHandler(IdleHandler* pHandler); - void SetPreFxRenderHandler(PreFxRenderHandler* pHandler); - void SetPostColorFilterRenderHandler(PostColorFilterRenderHandler* pHandler) override; - void SetPreHudRenderHandler(PreHudRenderHandler* pHandler); - void DisableCallsToCAnimBlendNode(bool bDisableCalls); - void SetCAnimBlendAssocDestructorHandler(CAnimBlendAssocDestructorHandler* pHandler); - void SetAddAnimationHandler(AddAnimationHandler* pHandler); - void SetAddAnimationAndSyncHandler(AddAnimationAndSyncHandler* pHandler); - void SetAssocGroupCopyAnimationHandler(AssocGroupCopyAnimationHandler* pHandler); - void SetBlendAnimationHierarchyHandler(BlendAnimationHierarchyHandler* pHandler); - void SetProcessCollisionHandler(ProcessCollisionHandler* pHandler); - void SetVehicleCollisionHandler(VehicleCollisionHandler* pHandler); - void SetVehicleDamageHandler(VehicleDamageHandler* pHandler); - void SetHeliKillHandler(HeliKillHandler* pHandler); - void SetObjectDamageHandler(ObjectDamageHandler* pHandler); - void SetObjectBreakHandler(ObjectBreakHandler* pHandler); - void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler); - void* CreateCustomWaterCannon(); - void DestroyCustomWaterCannon(void* pCannon); - void UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity); - void SetVehicleFellThroughMapHandler(VehicleFellThroughMapHandler* pHandler); - void SetGameObjectDestructHandler(GameObjectDestructHandler* pHandler); - void SetGameVehicleDestructHandler(GameVehicleDestructHandler* pHandler); - void SetGamePlayerDestructHandler(GamePlayerDestructHandler* pHandler); - void SetGameProjectileDestructHandler(GameProjectileDestructHandler* pHandler); - void SetGameModelRemoveHandler(GameModelRemoveHandler* pHandler); - void SetGameRunNamedAnimDestructorHandler(GameRunNamedAnimDestructorHandler* pHandler); - void SetGameEntityRenderHandler(GameEntityRenderHandler* pHandler); - void SetFxSystemDestructionHandler(FxSystemDestructionHandler* pHandler); - void SetDrivebyAnimationHandler(DrivebyAnimationHandler* pHandler); - void SetPedStepHandler(PedStepHandler* pHandler); - void SetVehicleWeaponHitHandler(VehicleWeaponHitHandler* pHandler) override; - void SetAudioZoneRadioSwitchHandler(AudioZoneRadioSwitchHandler* pHandler); + bool GetExplosionsDisabled(); + void DisableExplosions(bool bDisabled); + void SetExplosionHandler(ExplosionHandler* pExplosionHandler); + void SetDamageHandler(DamageHandler* pDamageHandler); + void SetDeathHandler(DeathHandler* pDeathHandler); + void SetProjectileHandler(ProjectileHandler* pProjectileHandler); + void SetProjectileStopHandler(ProjectileStopHandler* pProjectileHandler); + void SetFireHandler(FireHandler* pFireHandler); + void SetBreakTowLinkHandler(BreakTowLinkHandler* pBreakTowLinkHandler); + void SetProcessCamHandler(ProcessCamHandler* pProcessCamHandler); + void SetChokingHandler(ChokingHandler* pChokingHandler); + void SetPreWeatherUpdateHandler(PreWeatherUpdateHandler* pHandler); + void SetPreWorldProcessHandler(PreWorldProcessHandler* pHandler); + void SetPostWorldProcessHandler(PostWorldProcessHandler* pHandler); + void SetPostWorldProcessPedsAfterPreRenderHandler(PostWorldProcessPedsAfterPreRenderHandler* pHandler); + void SetIdleHandler(IdleHandler* pHandler); + void SetPreFxRenderHandler(PreFxRenderHandler* pHandler); + void SetPostColorFilterRenderHandler(PostColorFilterRenderHandler* pHandler) override; + void SetPreHudRenderHandler(PreHudRenderHandler* pHandler); + void DisableCallsToCAnimBlendNode(bool bDisableCalls); + void SetCAnimBlendAssocDestructorHandler(CAnimBlendAssocDestructorHandler* pHandler); + void SetAddAnimationHandler(AddAnimationHandler* pHandler); + void SetAddAnimationAndSyncHandler(AddAnimationAndSyncHandler* pHandler); + void SetAssocGroupCopyAnimationHandler(AssocGroupCopyAnimationHandler* pHandler); + void SetBlendAnimationHierarchyHandler(BlendAnimationHierarchyHandler* pHandler); + void SetProcessCollisionHandler(ProcessCollisionHandler* pHandler); + void SetVehicleCollisionHandler(VehicleCollisionHandler* pHandler); + void SetVehicleDamageHandler(VehicleDamageHandler* pHandler); + void SetHeliKillHandler(HeliKillHandler* pHandler); + void SetObjectDamageHandler(ObjectDamageHandler* pHandler); + void SetObjectBreakHandler(ObjectBreakHandler* pHandler); + void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler); + void* CreateCustomWaterCannon() override; + void DestroyCustomWaterCannon(void* pCannon) override; + void UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity) override; + void SetCustomWaterCannonColor(void* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, unsigned char ucAlpha) override; + void ResetCustomWaterCannonColor(void* pCannon) override; + void SetVehicleFellThroughMapHandler(VehicleFellThroughMapHandler* pHandler); + void SetGameObjectDestructHandler(GameObjectDestructHandler* pHandler); + void SetGameVehicleDestructHandler(GameVehicleDestructHandler* pHandler); + void SetGamePlayerDestructHandler(GamePlayerDestructHandler* pHandler); + void SetGameProjectileDestructHandler(GameProjectileDestructHandler* pHandler); + void SetGameModelRemoveHandler(GameModelRemoveHandler* pHandler); + void SetGameRunNamedAnimDestructorHandler(GameRunNamedAnimDestructorHandler* pHandler); + void SetGameEntityRenderHandler(GameEntityRenderHandler* pHandler); + void SetFxSystemDestructionHandler(FxSystemDestructionHandler* pHandler); + void SetDrivebyAnimationHandler(DrivebyAnimationHandler* pHandler); + void SetPedStepHandler(PedStepHandler* pHandler); + void SetVehicleWeaponHitHandler(VehicleWeaponHitHandler* pHandler) override; + void SetAudioZoneRadioSwitchHandler(AudioZoneRadioSwitchHandler* pHandler); void AllowMouseMovement(bool bAllow); void DoSoundHacksOnLostFocus(bool bLostFocus); diff --git a/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp b/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp index 5c4b383dd63..1b2068dc153 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp @@ -10,6 +10,8 @@ *****************************************************************************/ #include "StdInc.h" #include +#include +#include #include // Native CWaterCannon (per-instance) methods. The Firetruck/SWAT tank's own water cannons live in @@ -25,12 +27,37 @@ #define FUNC_CWaterCannon_Render 0x728DA0 #define SIZE_CWaterCannon 0x3CC +// The jet's audio entity is embedded at this offset; its per-frame Service is what the stock +// CWaterCannons::Update calls for the vehicle-owned cannons, and Initialise wires its owner entity +#define OFFSET_CWaterCannon_Audio 0x32C +#define FUNC_CAEWaterCannonAudioEntity_Initialise 0x503060 +#define FUNC_CAEWaterCannonAudioEntity_Service 0x5030D0 + +// The jet-sound gate in Service is "section in use AND owner vehicle (cannon+0) != 0". Our +// vehicle-less cannons have no owner, so this je would mute them; NOP it to drop just the owner +// half of the gate. Firetruck cannons always have an owner, so their behaviour is unchanged. +#define PATCH_CWaterCannon_SoundGateOwnerCheck 0x503127 + // Not a native constraint, since these live entirely outside the vehicles' own fixed-size array; // just a sanity ceiling so a runaway script can't grow this list without bound. Update_OncePerFrame // runs a world-wide ped scan for every entry every few frames, so this is also a soft cost limit. static constexpr size_t MAX_CUSTOM_WATER_CANNONS = 256; -static std::vector customWaterCannons; +struct SWaterCannonColor +{ + unsigned char r; + unsigned char g; + unsigned char b; + unsigned char a; +}; + +static std::vector customWaterCannons; +static std::map customWaterCannonColors; + +// Which of our cannons is being rendered right now, so the shared native Render colour code knows +// whose colour to use; null while the game renders the vehicle-owned cannons, which keep the +// native colour. Set around each of our own Render calls, never fished out of registers +static void* g_pRenderingCustomCannon = nullptr; void* CMultiplayerSA::CreateCustomWaterCannon() { @@ -42,6 +69,11 @@ void* CMultiplayerSA::CreateCustomWaterCannon() ((void(__thiscall*)(void*))FUNC_CWaterCannon_Constructor)(pCannon); ((void(__thiscall*)(void*))FUNC_CWaterCannon_Init)(pCannon); + // Point the embedded audio entity at this cannon and initialise it, so its per-frame Service + // has a valid owner to read from and passes its sound-bank checks; a vehicle-less cannon whose + // audio was never wired up this way stays silent + ((void(__thiscall*)(void*, void*))FUNC_CAEWaterCannonAudioEntity_Initialise)(static_cast(pCannon) + OFFSET_CWaterCannon_Audio, pCannon); + customWaterCannons.push_back(pCannon); return pCannon; } @@ -56,6 +88,7 @@ void CMultiplayerSA::DestroyCustomWaterCannon(void* pCannon) return; customWaterCannons.erase(iter); + customWaterCannonColors.erase(pCannon); ((void(__thiscall*)(void*))FUNC_CWaterCannon_Destructor)(pCannon); free(pCannon); @@ -69,17 +102,45 @@ void CMultiplayerSA::UpdateCustomWaterCannon(void* pCannon, const CVector& vecSt ((void(__thiscall*)(void*, const CVector*, const CVector*))FUNC_CWaterCannon_UpdateNewInput)(pCannon, &vecStart, &vecVelocity); } +void CMultiplayerSA::SetCustomWaterCannonColor(void* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, unsigned char ucAlpha) +{ + if (!pCannon) + return; + + customWaterCannonColors[pCannon] = {ucRed, ucGreen, ucBlue, ucAlpha}; +} + +void CMultiplayerSA::ResetCustomWaterCannonColor(void* pCannon) +{ + if (!pCannon) + return; + + customWaterCannonColors.erase(pCannon); +} + static void UpdateAllCustomWaterCannons() { std::int16_t index = 0; for (void* pCannon : customWaterCannons) + { + // Service the audio BEFORE the per-frame update, the same order CWaterCannons::Update uses. + // UpdateOncePerFrame advances m_nSectionsCount onto a freshly cleared section, so servicing + // after it would see that section unused and restart the jet sound every frame (a machine + // gun stutter). + ((void(__thiscall*)(void*))FUNC_CAEWaterCannonAudioEntity_Service)(static_cast(pCannon) + OFFSET_CWaterCannon_Audio); + ((void(__thiscall*)(void*, std::int16_t))FUNC_CWaterCannon_UpdateOncePerFrame)(pCannon, index++); + } } static void RenderAllCustomWaterCannons() { for (void* pCannon : customWaterCannons) + { + g_pRenderingCustomCannon = pCannon; ((void(__thiscall*)(void*))FUNC_CWaterCannon_Render)(pCannon); + } + g_pRenderingCustomCannon = nullptr; } ////////////////////////////////////////////////////////////////////////////////////////// @@ -135,6 +196,56 @@ static void __declspec(naked) HOOK_CWaterCannons_RenderCustomDispatch() // clang-format on } +////////////////////////////////////////////////////////////////////////////////////////// +// CWaterCannon::Render packs a hardcoded RGBA (R=200 G=200 B=255, alpha fades along the jet) +// into every vertex; override it per instance so setWaterCannonColor works. The block runs for +// every cannon, and eax holds the jet's fade alpha (0..64) on entry. The cannon being rendered +// is taken from g_pRenderingCustomCannon (set around our own Render calls), so a null one (a +// vehicle-owned cannon) or one without a custom colour keeps the native light blue. +////////////////////////////////////////////////////////////////////////////////////////// +// >>> 0x7290FF | C1 E0 18 | shl eax, 0x18 +// 0x729102 | 8B C8 | mov ecx, eax +// 0x729104 | 81 C9 FF C8 C8 00 | or ecx, 0x00C8C8FF (11 bytes total, rejoin at 0x72910A) +static DWORD __cdecl ComputeCustomWaterCannonColor(int iFadeAlpha) +{ + const DWORD dwFade = static_cast(iFadeAlpha & 0xFF) << 24; + + if (!g_pRenderingCustomCannon) + return dwFade | 0x00C8C8FF; // native default (R200 G200 B255) + + const auto iter = customWaterCannonColors.find(g_pRenderingCustomCannon); + if (iter == customWaterCannonColors.end()) + return dwFade | 0x00C8C8FF; + + // Keep the jet's own fade, scaled by the chosen alpha, and swap in the chosen RGB + const SWaterCannonColor& color = iter->second; + const DWORD dwAlpha = static_cast((iFadeAlpha * color.a / 255) & 0xFF) << 24; + return dwAlpha | (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); +} + +#define HOOKPOS_CWaterCannon_RenderColor 0x7290FF +#define HOOKSIZE_CWaterCannon_RenderColor 11 +static const DWORD RETURN_CWaterCannon_RenderColor = 0x72910A; + +static void __declspec(naked) HOOK_CWaterCannon_RenderColor() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + push edx // the lookup call clobbers edx, which is live past the rejoin + push eax // arg: fade alpha + call ComputeCustomWaterCannonColor + add esp, 4 + pop edx + mov ecx, eax // the vertex store loop at the rejoin expects the colour in ecx + mov eax, RETURN_CWaterCannon_RenderColor + jmp eax + } + // clang-format on +} + ////////////////////////////////////////////////////////////////////////////////////////// // // CMultiplayerSA::InitHooks_WaterCannons @@ -144,4 +255,7 @@ void CMultiplayerSA::InitHooks_WaterCannons() { EZHookInstall(CWaterCannons_UpdateCustomDispatch); EZHookInstall(CWaterCannons_RenderCustomDispatch); + EZHookInstall(CWaterCannon_RenderColor); + + MemSet(reinterpret_cast(PATCH_CWaterCannon_SoundGateOwnerCheck), 0x90, 2); } diff --git a/Client/sdk/multiplayer/CMultiplayer.h b/Client/sdk/multiplayer/CMultiplayer.h index 0023ffc3771..85efe743004 100644 --- a/Client/sdk/multiplayer/CMultiplayer.h +++ b/Client/sdk/multiplayer/CMultiplayer.h @@ -218,38 +218,38 @@ class CMultiplayer virtual void DisableCloseRangeDamage(bool bDisable) = 0; virtual void DisableBadDrivebyHitboxes(bool bDisable) = 0; - virtual bool GetExplosionsDisabled() = 0; - virtual void DisableExplosions(bool bDisabled) = 0; - virtual void SetExplosionHandler(ExplosionHandler* pExplosionHandler) = 0; - virtual void SetBreakTowLinkHandler(BreakTowLinkHandler* pBreakTowLinkHandler) = 0; - virtual void SetDamageHandler(DamageHandler* pDamageHandler) = 0; - virtual void SetDeathHandler(DeathHandler* pDeathHandler) = 0; - virtual void SetFireHandler(FireHandler* pFireHandler) = 0; - virtual void SetProcessCamHandler(ProcessCamHandler* pProcessCamHandler) = 0; - virtual void SetChokingHandler(ChokingHandler* pChokingHandler) = 0; - virtual void SetProjectileHandler(ProjectileHandler* pProjectileHandler) = 0; - virtual void SetProjectileStopHandler(ProjectileStopHandler* pProjectileHandler) = 0; - virtual void SetPreWeatherUpdateHandler(PreWeatherUpdateHandler* pHandler) = 0; - virtual void SetPreWorldProcessHandler(PreWorldProcessHandler* pHandler) = 0; - virtual void SetPostWorldProcessHandler(PostWorldProcessHandler* pHandler) = 0; - virtual void SetPostWorldProcessPedsAfterPreRenderHandler(PostWorldProcessPedsAfterPreRenderHandler* pHandler) = 0; - virtual void SetIdleHandler(IdleHandler* pHandler) = 0; - virtual void SetPreFxRenderHandler(PreFxRenderHandler* pHandler) = 0; - virtual void SetPostColorFilterRenderHandler(PostColorFilterRenderHandler* pHandler) = 0; - virtual void SetPreHudRenderHandler(PreHudRenderHandler* pHandler) = 0; - virtual void DisableCallsToCAnimBlendNode(bool bDisableCalls) = 0; - virtual void SetCAnimBlendAssocDestructorHandler(CAnimBlendAssocDestructorHandler* pHandler) = 0; - virtual void SetAddAnimationHandler(AddAnimationHandler* pHandler) = 0; - virtual void SetAddAnimationAndSyncHandler(AddAnimationAndSyncHandler* pHandler) = 0; - virtual void SetAssocGroupCopyAnimationHandler(AssocGroupCopyAnimationHandler* pHandler) = 0; - virtual void SetBlendAnimationHierarchyHandler(BlendAnimationHierarchyHandler* pHandler) = 0; - virtual void SetProcessCollisionHandler(ProcessCollisionHandler* pHandler) = 0; - virtual void SetVehicleCollisionHandler(VehicleCollisionHandler* pHandler) = 0; - virtual void SetVehicleDamageHandler(VehicleDamageHandler* pHandler) = 0; - virtual void SetHeliKillHandler(HeliKillHandler* pHandler) = 0; - virtual void SetObjectDamageHandler(ObjectDamageHandler* pHandler) = 0; - virtual void SetObjectBreakHandler(ObjectBreakHandler* pHandler) = 0; - virtual void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler) = 0; + virtual bool GetExplosionsDisabled() = 0; + virtual void DisableExplosions(bool bDisabled) = 0; + virtual void SetExplosionHandler(ExplosionHandler* pExplosionHandler) = 0; + virtual void SetBreakTowLinkHandler(BreakTowLinkHandler* pBreakTowLinkHandler) = 0; + virtual void SetDamageHandler(DamageHandler* pDamageHandler) = 0; + virtual void SetDeathHandler(DeathHandler* pDeathHandler) = 0; + virtual void SetFireHandler(FireHandler* pFireHandler) = 0; + virtual void SetProcessCamHandler(ProcessCamHandler* pProcessCamHandler) = 0; + virtual void SetChokingHandler(ChokingHandler* pChokingHandler) = 0; + virtual void SetProjectileHandler(ProjectileHandler* pProjectileHandler) = 0; + virtual void SetProjectileStopHandler(ProjectileStopHandler* pProjectileHandler) = 0; + virtual void SetPreWeatherUpdateHandler(PreWeatherUpdateHandler* pHandler) = 0; + virtual void SetPreWorldProcessHandler(PreWorldProcessHandler* pHandler) = 0; + virtual void SetPostWorldProcessHandler(PostWorldProcessHandler* pHandler) = 0; + virtual void SetPostWorldProcessPedsAfterPreRenderHandler(PostWorldProcessPedsAfterPreRenderHandler* pHandler) = 0; + virtual void SetIdleHandler(IdleHandler* pHandler) = 0; + virtual void SetPreFxRenderHandler(PreFxRenderHandler* pHandler) = 0; + virtual void SetPostColorFilterRenderHandler(PostColorFilterRenderHandler* pHandler) = 0; + virtual void SetPreHudRenderHandler(PreHudRenderHandler* pHandler) = 0; + virtual void DisableCallsToCAnimBlendNode(bool bDisableCalls) = 0; + virtual void SetCAnimBlendAssocDestructorHandler(CAnimBlendAssocDestructorHandler* pHandler) = 0; + virtual void SetAddAnimationHandler(AddAnimationHandler* pHandler) = 0; + virtual void SetAddAnimationAndSyncHandler(AddAnimationAndSyncHandler* pHandler) = 0; + virtual void SetAssocGroupCopyAnimationHandler(AssocGroupCopyAnimationHandler* pHandler) = 0; + virtual void SetBlendAnimationHierarchyHandler(BlendAnimationHierarchyHandler* pHandler) = 0; + virtual void SetProcessCollisionHandler(ProcessCollisionHandler* pHandler) = 0; + virtual void SetVehicleCollisionHandler(VehicleCollisionHandler* pHandler) = 0; + virtual void SetVehicleDamageHandler(VehicleDamageHandler* pHandler) = 0; + virtual void SetHeliKillHandler(HeliKillHandler* pHandler) = 0; + virtual void SetObjectDamageHandler(ObjectDamageHandler* pHandler) = 0; + virtual void SetObjectBreakHandler(ObjectBreakHandler* pHandler) = 0; + virtual void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler) = 0; // A script-owned water cannon, driven by its own list of native CWaterCannon instances kept // entirely apart from the vehicle-owned ones (the Firetruck/SWAT tank's own aCannons array), @@ -257,6 +257,8 @@ class CMultiplayer virtual void* CreateCustomWaterCannon() = 0; virtual void DestroyCustomWaterCannon(void* pCannon) = 0; virtual void UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity) = 0; + virtual void SetCustomWaterCannonColor(void* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, unsigned char ucAlpha) = 0; + virtual void ResetCustomWaterCannonColor(void* pCannon) = 0; virtual void SetVehicleFellThroughMapHandler(VehicleFellThroughMapHandler* pHandler) = 0; virtual void SetGameObjectDestructHandler(GameObjectDestructHandler* pHandler) = 0; virtual void SetGameVehicleDestructHandler(GameVehicleDestructHandler* pHandler) = 0; From 79b529ab83a073193545800bd579a41b7621d52a Mon Sep 17 00:00:00 2001 From: TheCrazy17 Date: Wed, 2 Sep 2026 08:15:10 -0300 Subject: [PATCH 3/4] Let custom water cannons knock peds down like a real vehicle cannon onClientPedHitByWaterCannon's handler unconditionally cancelled the native push and event whenever it could not resolve a cannon vehicle, which was always the case for a script-owned standalone cannon; it now only asks the water cannon manager whether that particular cannon has knockdown enabled, and createWaterCannon plus setWaterCannonKnockdownEnabled/ isWaterCannonKnockdownEnabled let a script opt back out per cannon. --- Client/mods/deathmatch/logic/CClientGame.cpp | 12 +++++++--- Client/mods/deathmatch/logic/CClientGame.h | 4 ++-- .../deathmatch/logic/CClientWaterCannon.cpp | 1 + .../deathmatch/logic/CClientWaterCannon.h | 9 +++++++- .../logic/CClientWaterCannonManager.cpp | 10 +++++++++ .../logic/CClientWaterCannonManager.h | 5 +++++ .../logic/luadefs/CLuaWaterCannonDefs.cpp | 22 ++++++++++++++++++- .../logic/luadefs/CLuaWaterCannonDefs.h | 6 ++++- Client/multiplayer_sa/CMultiplayerSA_1.3.cpp | 4 +++- Client/sdk/multiplayer/CMultiplayer.h | 2 +- 10 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 14cfe869285..fcd30d55bab 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -3714,9 +3714,9 @@ bool CClientGame::StaticObjectBreakHandler(CObjectSAInterface* pObjectInterface, return g_pClientGame->ObjectBreakHandler(pObjectInterface, pAttackerInterface); } -bool CClientGame::StaticWaterCannonHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed) +bool CClientGame::StaticWaterCannonHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface) { - return g_pClientGame->WaterCannonHitHandler(pCannonVehicle, pHitPed); + return g_pClientGame->WaterCannonHitHandler(pCannonVehicle, pHitPed, pWaterCannonInterface); } bool CClientGame::StaticVehicleFellThroughMapHandler(CVehicleSAInterface* pVehicle) @@ -4938,8 +4938,14 @@ bool CClientGame::ObjectBreakHandler(CObjectSAInterface* pObjectInterface, CEnti return true; } -bool CClientGame::WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed) +bool CClientGame::WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface) { + // A script-owned standalone cannon (createWaterCannon) has no vehicle to fire + // onClientPedHitByWaterCannon/onClientPlayerHitByWaterCannon against; let the native push and + // knockdown through as normal unless the script disabled it for this particular cannon + if (!pCannonVehicle) + return m_pManager->GetWaterCannonManager()->IsKnockdownEnabled(pWaterCannonInterface); + if (pCannonVehicle && pHitPed) { CPools* pPools = g_pGame->GetPools(); diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index cf8048eb67c..341871bef18 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -601,7 +601,7 @@ class CClientGame static bool StaticHeliKillHandler(CVehicleSAInterface* pHeli, CEntitySAInterface* pHitInterface); static bool StaticObjectDamageHandler(CObjectSAInterface* pObjectInterface, float fLoss, CEntitySAInterface* pAttackerInterface); static bool StaticObjectBreakHandler(CObjectSAInterface* pObjectInterface, CEntitySAInterface* pAttackerInterface); - static bool StaticWaterCannonHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed); + static bool StaticWaterCannonHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface); static bool StaticVehicleFellThroughMapHandler(CVehicleSAInterface* pVehicle); static void StaticGameObjectDestructHandler(CEntitySAInterface* pObject); static void StaticGameVehicleDestructHandler(CEntitySAInterface* pVehicle); @@ -648,7 +648,7 @@ class CClientGame bool HeliKillHandler(CVehicleSAInterface* pHeli, CEntitySAInterface* pHitInterface); bool ObjectDamageHandler(CObjectSAInterface* pObjectInterface, float fLoss, CEntitySAInterface* pAttackerInterface); bool ObjectBreakHandler(CObjectSAInterface* pObjectInterface, CEntitySAInterface* pAttackerInterface); - bool WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed); + bool WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface); bool VehicleFellThroughMapHandler(CVehicleSAInterface* pVehicle); void GameObjectDestructHandler(CEntitySAInterface* pObject); void GameVehicleDestructHandler(CEntitySAInterface* pVehicle); diff --git a/Client/mods/deathmatch/logic/CClientWaterCannon.cpp b/Client/mods/deathmatch/logic/CClientWaterCannon.cpp index 77f6da34214..32d96519b7e 100644 --- a/Client/mods/deathmatch/logic/CClientWaterCannon.cpp +++ b/Client/mods/deathmatch/logic/CClientWaterCannon.cpp @@ -17,6 +17,7 @@ CClientWaterCannon::CClientWaterCannon(CClientManager* pManager, ElementID ID) : m_vecDirection = CVector(0.0f, 1.0f, 0.0f); m_fForce = 1.0f; m_bEnabled = true; + m_bKnockdownEnabled = true; m_bHasCustomColor = false; m_Color = SColor(0xFFC8C8FF); // the native jet's light blue (R200 G200 B255), opaque m_pNativeCannon = g_pMultiplayer->CreateCustomWaterCannon(); diff --git a/Client/mods/deathmatch/logic/CClientWaterCannon.h b/Client/mods/deathmatch/logic/CClientWaterCannon.h index 7def9b9804e..62d29dfc8bc 100644 --- a/Client/mods/deathmatch/logic/CClientWaterCannon.h +++ b/Client/mods/deathmatch/logic/CClientWaterCannon.h @@ -54,6 +54,11 @@ class CClientWaterCannon final : public CClientEntity bool IsEnabled() const { return m_bEnabled; }; void SetEnabled(bool bEnabled) { m_bEnabled = bEnabled; }; + // Whether getting sprayed by this cannon knocks a ped down, the same reaction a real vehicle- + // owned cannon gives; on by default to match that vanilla behaviour + bool IsKnockdownEnabled() const { return m_bKnockdownEnabled; }; + void SetKnockdownEnabled(bool bEnabled) { m_bKnockdownEnabled = bEnabled; }; + // A custom jet colour; when unset the native fading light-blue is used bool HasCustomColor() const { return m_bHasCustomColor; }; const SColor& GetColor() const { return m_Color; }; @@ -62,7 +67,8 @@ class CClientWaterCannon final : public CClientEntity // False if the shared custom-cannon pool (CMultiplayer::CreateCustomWaterCannon, capped // separately from the vehicle-owned one) was full at construction time. - bool HasNativeCannon() const { return m_pNativeCannon != nullptr; }; + bool HasNativeCannon() const { return m_pNativeCannon != nullptr; }; + void* GetNativeHandle() const { return m_pNativeCannon; }; protected: void DoPulse(); @@ -74,6 +80,7 @@ class CClientWaterCannon final : public CClientEntity CVector m_vecDirection; float m_fForce; bool m_bEnabled; + bool m_bKnockdownEnabled; bool m_bHasCustomColor; SColor m_Color; diff --git a/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp b/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp index d184fba0551..acc80d77cd7 100644 --- a/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp @@ -62,6 +62,16 @@ void CClientWaterCannonManager::DoPulse() pCannon->DoPulse(); } +bool CClientWaterCannonManager::IsKnockdownEnabled(void* pWaterCannonInterface) const +{ + for (CClientWaterCannon* pCannon : m_List) + { + if (pCannon->GetNativeHandle() == pWaterCannonInterface) + return pCannon->IsKnockdownEnabled(); + } + return true; +} + void CClientWaterCannonManager::RemoveFromList(CClientWaterCannon* pCannon) { if (!m_bDontRemoveFromList && !m_List.empty()) diff --git a/Client/mods/deathmatch/logic/CClientWaterCannonManager.h b/Client/mods/deathmatch/logic/CClientWaterCannonManager.h index 3fad714fe98..51bafaf932c 100644 --- a/Client/mods/deathmatch/logic/CClientWaterCannonManager.h +++ b/Client/mods/deathmatch/logic/CClientWaterCannonManager.h @@ -33,6 +33,11 @@ class CClientWaterCannonManager void DoPulse(); + // pWaterCannonInterface is the raw native CWaterCannon this cannon wraps (CClientWaterCannon:: + // GetNativeHandle); defaults to true for a handle this manager doesn't recognise, so a real + // vehicle-owned cannon's own knockdown is never accidentally blocked through this path + bool IsKnockdownEnabled(void* pWaterCannonInterface) const; + private: void AddToList(CClientWaterCannon* pCannon) { m_List.push_back(pCannon); }; void RemoveFromList(CClientWaterCannon* pCannon); diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp index 2c24e3fb924..c289e9394c7 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp @@ -21,6 +21,8 @@ void CLuaWaterCannonDefs::LoadFunctions() {"getWaterCannonForce", ArgumentParser}, {"setWaterCannonEnabled", ArgumentParser}, {"isWaterCannonEnabled", ArgumentParser}, + {"setWaterCannonKnockdownEnabled", ArgumentParser}, + {"isWaterCannonKnockdownEnabled", ArgumentParser}, {"setWaterCannonColor", ArgumentParser}, {"getWaterCannonColor", ArgumentParser}, {"resetWaterCannonColor", ArgumentParser}, @@ -39,22 +41,26 @@ void CLuaWaterCannonDefs::AddClass(lua_State* luaVM) lua_classfunction(luaVM, "setDirection", "setWaterCannonDirection"); lua_classfunction(luaVM, "setForce", "setWaterCannonForce"); lua_classfunction(luaVM, "setEnabled", "setWaterCannonEnabled"); + lua_classfunction(luaVM, "setKnockdownEnabled", "setWaterCannonKnockdownEnabled"); lua_classfunction(luaVM, "setColor", "setWaterCannonColor"); lua_classfunction(luaVM, "resetColor", "resetWaterCannonColor"); lua_classfunction(luaVM, "getDirection", "getWaterCannonDirection"); lua_classfunction(luaVM, "getForce", "getWaterCannonForce"); lua_classfunction(luaVM, "isEnabled", "isWaterCannonEnabled"); + lua_classfunction(luaVM, "isKnockdownEnabled", "isWaterCannonKnockdownEnabled"); lua_classfunction(luaVM, "getColor", "getWaterCannonColor"); lua_classvariable(luaVM, "direction", "setWaterCannonDirection", "getWaterCannonDirection"); lua_classvariable(luaVM, "force", "setWaterCannonForce", "getWaterCannonForce"); lua_classvariable(luaVM, "enabled", "setWaterCannonEnabled", "isWaterCannonEnabled"); + lua_classvariable(luaVM, "knockdownEnabled", "setWaterCannonKnockdownEnabled", "isWaterCannonKnockdownEnabled"); lua_registerclass(luaVM, "WaterCannon", "Element"); } -std::variant CLuaWaterCannonDefs::CreateWaterCannon(lua_State* luaVM, CVector vecPosition, std::optional color) +std::variant CLuaWaterCannonDefs::CreateWaterCannon(lua_State* luaVM, CVector vecPosition, std::optional color, + std::optional bKnockdownEnabled) { CResource& resource = lua_getownerresource(luaVM); CClientWaterCannon* pCannon = CStaticFunctionDefinitions::CreateWaterCannon(resource, vecPosition); @@ -64,6 +70,9 @@ std::variant CLuaWaterCannonDefs::CreateWaterCannon(l if (color.has_value()) pCannon->SetColor(color.value()); + if (bKnockdownEnabled.has_value()) + pCannon->SetKnockdownEnabled(bKnockdownEnabled.value()); + if (CElementGroup* elementGroup = resource.GetElementGroup()) elementGroup->Add(pCannon); @@ -103,6 +112,17 @@ bool CLuaWaterCannonDefs::IsWaterCannonEnabled(CClientWaterCannon* pCannon) return pCannon->IsEnabled(); } +bool CLuaWaterCannonDefs::SetWaterCannonKnockdownEnabled(CClientWaterCannon* pCannon, bool bEnabled) +{ + pCannon->SetKnockdownEnabled(bEnabled); + return true; +} + +bool CLuaWaterCannonDefs::IsWaterCannonKnockdownEnabled(CClientWaterCannon* pCannon) +{ + return pCannon->IsKnockdownEnabled(); +} + bool CLuaWaterCannonDefs::SetWaterCannonColor(CClientWaterCannon* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, std::optional ucAlpha) { diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h index e2df88fe0eb..0775ec574c2 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h @@ -19,7 +19,8 @@ class CLuaWaterCannonDefs : public CLuaDefs static void LoadFunctions(); static void AddClass(lua_State* luaVM); - static std::variant CreateWaterCannon(lua_State* luaVM, CVector vecPosition, std::optional color); + static std::variant CreateWaterCannon(lua_State* luaVM, CVector vecPosition, std::optional color, + std::optional bKnockdownEnabled); static bool SetWaterCannonDirection(CClientWaterCannon* pCannon, CVector vecDirection); static CVector GetWaterCannonDirection(CClientWaterCannon* pCannon); @@ -30,6 +31,9 @@ class CLuaWaterCannonDefs : public CLuaDefs static bool SetWaterCannonEnabled(CClientWaterCannon* pCannon, bool bEnabled); static bool IsWaterCannonEnabled(CClientWaterCannon* pCannon); + static bool SetWaterCannonKnockdownEnabled(CClientWaterCannon* pCannon, bool bEnabled); + static bool IsWaterCannonKnockdownEnabled(CClientWaterCannon* pCannon); + static bool SetWaterCannonColor(CClientWaterCannon* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, std::optional ucAlpha); static CLuaMultiReturn GetWaterCannonColor(CClientWaterCannon* pCannon); diff --git a/Client/multiplayer_sa/CMultiplayerSA_1.3.cpp b/Client/multiplayer_sa/CMultiplayerSA_1.3.cpp index 2cc722219e4..c4b017ba1bd 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_1.3.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_1.3.cpp @@ -159,13 +159,14 @@ void CMultiplayerSA::SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler) CPedSAInterface* pPedHitByWaterCannonInterface = NULL; CVehicleSAInterface* pVehicleWithTheCannonMounted = NULL; +void* pWaterCannonHitBy = NULL; bool TriggerTheEvent() { // Is our handler alive if (m_pWaterCannonHitHandler) { // Return our handlers return - return !m_pWaterCannonHitHandler(pVehicleWithTheCannonMounted, pPedHitByWaterCannonInterface); + return !m_pWaterCannonHitHandler(pVehicleWithTheCannonMounted, pPedHitByWaterCannonInterface, pWaterCannonHitBy); } return false; } @@ -184,6 +185,7 @@ static void __declspec(naked) HOOK_CEventHitByWaterCannon() mov eax, [edx] mov pPedHitByWaterCannonInterface, esi mov pVehicleWithTheCannonMounted, eax + mov pWaterCannonHitBy, edx } // clang-format on if (TriggerTheEvent()) diff --git a/Client/sdk/multiplayer/CMultiplayer.h b/Client/sdk/multiplayer/CMultiplayer.h index 85efe743004..b8b4d6dffa9 100644 --- a/Client/sdk/multiplayer/CMultiplayer.h +++ b/Client/sdk/multiplayer/CMultiplayer.h @@ -123,7 +123,7 @@ typedef bool(VehicleDamageHandler)(CEntitySAInterface* pVehicle, float fLoss, CE typedef bool(HeliKillHandler)(class CVehicleSAInterface* pVehicle, class CEntitySAInterface* pHitInterface); typedef bool(ObjectDamageHandler)(class CObjectSAInterface* pObject, float fLoss, class CEntitySAInterface* pAttacker); typedef bool(ObjectBreakHandler)(class CObjectSAInterface* pObject, class CEntitySAInterface* pAttacker); -typedef bool(WaterCannonHitHandler)(class CVehicleSAInterface* pCannonVehicle, class CPedSAInterface* pHitPed); +typedef bool(WaterCannonHitHandler)(class CVehicleSAInterface* pCannonVehicle, class CPedSAInterface* pHitPed, void* pWaterCannonInterface); typedef bool(VehicleFellThroughMapHandler)(class CVehicleSAInterface* pVehicle); typedef void(GameObjectDestructHandler)(CEntitySAInterface* pObject); typedef void(GameVehicleDestructHandler)(CEntitySAInterface* pVehicle); From 7d6111a92169d017b9f05bc94b6f3559ac89b109 Mon Sep 17 00:00:00 2001 From: TheCrazy17 Date: Wed, 2 Sep 2026 21:08:09 -0300 Subject: [PATCH 4/4] Knock peds down and fire hit events for custom water cannons PushPeds hit-tests from a ped's feet with a tight ~2.24 unit radius, which only a vehicle cannon's sagging arc reliably reaches; a custom cannon aimed at someone's chest can miss every droplet despite visibly spraying them. Widen the radius only for vehicle-less cannons, and fire onClientPedHitByWaterCannon/onClientPlayerHitByWaterCannon from the cannon element itself, matching what a vehicle cannon already gives scripts. --- Client/mods/deathmatch/logic/CClientGame.cpp | 27 +++++++++-- .../logic/CClientWaterCannonManager.cpp | 6 +-- .../logic/CClientWaterCannonManager.h | 8 ++-- .../CMultiplayerSA_WaterCannons.cpp | 47 +++++++++++++++++++ 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index fcd30d55bab..94505355680 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -4941,10 +4941,31 @@ bool CClientGame::ObjectBreakHandler(CObjectSAInterface* pObjectInterface, CEnti bool CClientGame::WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface) { // A script-owned standalone cannon (createWaterCannon) has no vehicle to fire - // onClientPedHitByWaterCannon/onClientPlayerHitByWaterCannon against; let the native push and - // knockdown through as normal unless the script disabled it for this particular cannon + // onClientPedHitByWaterCannon/onClientPlayerHitByWaterCannon against; fire the same two events + // from the cannon element itself instead, so scripts get exactly the same notification (and the + // same chance to cancel the native knockdown) a real vehicle cannon gives them if (!pCannonVehicle) - return m_pManager->GetWaterCannonManager()->IsKnockdownEnabled(pWaterCannonInterface); + { + CClientWaterCannon* pWaterCannon = m_pManager->GetWaterCannonManager()->GetByNativeHandle(pWaterCannonInterface); + if (!pWaterCannon || !pWaterCannon->IsKnockdownEnabled()) + return false; + + CPools* pPools = g_pGame->GetPools(); + CClientPed* pClientPed = nullptr; + SClientEntity* pPedEntity = pPools->GetPed(reinterpret_cast(pHitPed)); + if (pPedEntity) + pClientPed = reinterpret_cast(pPedEntity->pClientEntity); + + CLuaArguments Arguments; + if (pClientPed) + Arguments.PushElement(pClientPed); + else + Arguments.PushNil(); + + if (pClientPed && !IS_PLAYER(pClientPed)) + return pWaterCannon->CallEvent("onClientPedHitByWaterCannon", Arguments, true); + return pWaterCannon->CallEvent("onClientPlayerHitByWaterCannon", Arguments, true); + } if (pCannonVehicle && pHitPed) { diff --git a/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp b/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp index acc80d77cd7..3a68c24c2a7 100644 --- a/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp @@ -62,14 +62,14 @@ void CClientWaterCannonManager::DoPulse() pCannon->DoPulse(); } -bool CClientWaterCannonManager::IsKnockdownEnabled(void* pWaterCannonInterface) const +CClientWaterCannon* CClientWaterCannonManager::GetByNativeHandle(void* pWaterCannonInterface) const { for (CClientWaterCannon* pCannon : m_List) { if (pCannon->GetNativeHandle() == pWaterCannonInterface) - return pCannon->IsKnockdownEnabled(); + return pCannon; } - return true; + return nullptr; } void CClientWaterCannonManager::RemoveFromList(CClientWaterCannon* pCannon) diff --git a/Client/mods/deathmatch/logic/CClientWaterCannonManager.h b/Client/mods/deathmatch/logic/CClientWaterCannonManager.h index 51bafaf932c..5e0fd1dc2f3 100644 --- a/Client/mods/deathmatch/logic/CClientWaterCannonManager.h +++ b/Client/mods/deathmatch/logic/CClientWaterCannonManager.h @@ -33,10 +33,10 @@ class CClientWaterCannonManager void DoPulse(); - // pWaterCannonInterface is the raw native CWaterCannon this cannon wraps (CClientWaterCannon:: - // GetNativeHandle); defaults to true for a handle this manager doesn't recognise, so a real - // vehicle-owned cannon's own knockdown is never accidentally blocked through this path - bool IsKnockdownEnabled(void* pWaterCannonInterface) const; + // Traces a hit back to the CClientWaterCannon that owns the raw native CWaterCannon instance + // (CClientWaterCannon::GetNativeHandle) it happened on; null for a handle this manager doesn't + // recognise (a real vehicle-owned cannon's native instance, most likely). + CClientWaterCannon* GetByNativeHandle(void* pWaterCannonInterface) const; private: void AddToList(CClientWaterCannon* pCannon) { m_List.push_back(pCannon); }; diff --git a/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp b/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp index 1b2068dc153..9ad7032f8a3 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp @@ -246,6 +246,52 @@ static void __declspec(naked) HOOK_CWaterCannon_RenderColor() // clang-format on } +////////////////////////////////////////////////////////////////////////////////////////// +// PushPeds only knocks down a ped standing within sqrt(5) (~2.24) units of one of the jet's own +// recorded droplet points, measured from the ped's feet - fine for a real vehicle cannon, since its +// arc has always sagged to near ground level by the time it reaches a target at a normal spray +// angle/range. A script can aim a custom cannon level with someone's chest instead, where the beam +// still visibly cuts off and splashes against them (Render's own hit test is a full-body raycast, +// not a feet-anchored point check), but PushPeds's tighter test can miss every single droplet. +// Widen the radius only when the cannon has no vehicle owner, so a real Firetruck/SWAT tank's own +// spray is untouched; a stale/dangling owner pointer from a destroyed vehicle can't misfire this, +// since PushPeds only ever runs against sections that are still genuinely in flight, and a vehicle +// cannon's owner field is only ever cleared once none of its sections are left. +////////////////////////////////////////////////////////////////////////////////////////// +// >>> 0x72987D | D8 1D 80 8C 85 00 | fcomp dword ptr [0x00858C80] +// 0x729883 | DF E0 | fnstsw ax +static constexpr float CUSTOM_WATER_CANNON_HIT_RADIUS_SQUARED = 1.0f; // radius 1.0, vs the native sqrt(5) (~2.24) + +#define HOOKPOS_CWaterCannon_PushPeds_HitRadius 0x72987D +#define HOOKSIZE_CWaterCannon_PushPeds_HitRadius 6 +static constexpr std::uintptr_t CONTINUE_CWaterCannon_PushPeds_HitRadius = 0x729883; + +static void __declspec(naked) HOOK_CWaterCannon_PushPeds_HitRadius() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + // [esp+0x1C] holds the cannon's own "this" pointer for the whole function (written once in + // the prologue); EDI, which held it earlier, has already been repurposed as a per-point + // record pointer by this point in the loop, so it can't be used here + mov ebx, [esp+0x1C] + cmp dword ptr [ebx], 0 + jne useOriginal + + fcomp CUSTOM_WATER_CANNON_HIT_RADIUS_SQUARED + jmp continue_ + + useOriginal: + fcomp ds:[0x858C80] // Original comparison, byte-identical to the stolen instruction + + continue_: + jmp CONTINUE_CWaterCannon_PushPeds_HitRadius + } + // clang-format on +} + ////////////////////////////////////////////////////////////////////////////////////////// // // CMultiplayerSA::InitHooks_WaterCannons @@ -256,6 +302,7 @@ void CMultiplayerSA::InitHooks_WaterCannons() EZHookInstall(CWaterCannons_UpdateCustomDispatch); EZHookInstall(CWaterCannons_RenderCustomDispatch); EZHookInstall(CWaterCannon_RenderColor); + EZHookInstall(CWaterCannon_PushPeds_HitRadius); MemSet(reinterpret_cast(PATCH_CWaterCannon_SoundGateOwnerCheck), 0x90, 2); }